Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 352c8c6615 | |||
| 2c07a42bdc | |||
| 3ab01c98c8 | |||
| 6445c05e29 | |||
| b9b25eb1f6 | |||
| de157b4f52 | |||
| e5581e52f7 | |||
| b91840c819 | |||
| a9b77fb9a7 | |||
| 66f83db0e5 | |||
| f9ff578c6b | |||
| 1372438f8e | |||
| c5d5d3dec4 | |||
| ad16c0843c | |||
| 78a6307656 | |||
| d7d468f51f | |||
| eb71028f6b | |||
| 39c22b074d | |||
| d5c3f248a5 | |||
| 91217b3c4f | |||
| ab271bc90d | |||
| 82e1783317 | |||
| aa2b9ed5f2 | |||
| 1c96d645ba | |||
| 2f010cf36d | |||
| 9d3c1eb9d5 | |||
| 08f3ceaf3f | |||
| b1d033edc9 | |||
| 192fa76b54 | |||
| b667200ebd | |||
| 294922f966 | |||
| 8b73aa3644 | |||
| dd56a8abeb | |||
| 145c688830 | |||
| 950d215632 | |||
| 7d5cc4e84b | |||
| 3e5bcf96ea | |||
| fe24c00178 | |||
| aca334f81f | |||
| 2003e41c22 | |||
| 5ebdf4b4d4 | |||
| 35e771a1ce | |||
| 2b5a9e1af8 | |||
| a833fdc7a1 | |||
| b3cc2bf833 | |||
| 18feab10cb | |||
| 2777488d24 |
@@ -75,3 +75,6 @@ db.sqlite3
|
||||
.vscode/
|
||||
*.iml
|
||||
.devcontainer
|
||||
|
||||
# Cursor rules
|
||||
.cursorrules
|
||||
|
||||
+38
-3
@@ -6,11 +6,37 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [3.9.0] - 2025-11-10
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) create skeleton component for DocEditor #1491
|
||||
- ✨(frontend) add an EmojiPicker in the document tree and title #1381
|
||||
- ✨(frontend) ajustable left panel #1456
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️(frontend) adapt custom blocks to new implementation #1375
|
||||
- ♻️(backend) increase user short_name field length #1510
|
||||
- 🚸(frontend) separate viewers from editors #1509
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) fix duplicate document entries in grid #1479
|
||||
- 🐛(backend) fix trashbin list #1520
|
||||
- ♿(frontend) improve accessibility:
|
||||
- ♿(frontend) remove empty alt on logo due to Axe a11y error #1516
|
||||
- 🐛(backend) fix s3 version_id validation #1543
|
||||
- 🐛(frontend) retry check media status after page reload #1555
|
||||
- 🐛(frontend) fix Interlinking memory leak #1560
|
||||
- 🐛(frontend) button new doc UI fix #1557
|
||||
- 🐛(frontend) interlinking UI fix #1557
|
||||
|
||||
## [3.8.2] - 2025-10-17
|
||||
|
||||
### Fixed
|
||||
|
||||
🐛(service-worker) fix sw registration and page reload logic #1500
|
||||
- 🐛(service-worker) fix sw registration and page reload logic #1500
|
||||
|
||||
## [3.8.1] - 2025-10-17
|
||||
|
||||
@@ -26,7 +52,6 @@ and this project adheres to
|
||||
|
||||
- 🔥(backend) remove treebeard form for the document admin #1470
|
||||
|
||||
|
||||
## [3.8.0] - 2025-10-14
|
||||
|
||||
### Added
|
||||
@@ -76,11 +101,18 @@ and this project adheres to
|
||||
- ✨(frontend) load docs logo from public folder via url #1462
|
||||
- 🔧(keycloak) Fix https required issue in dev mode #1286
|
||||
|
||||
## Removed
|
||||
|
||||
- 🔥(frontend) remove custom DividerBlock ##1375
|
||||
|
||||
## [3.7.0] - 2025-09-12
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(api) add API route to fetch document content #1206
|
||||
- ✨(frontend) doc emojis improvements #1381
|
||||
- add an EmojiPicker in the document tree and document title
|
||||
- remove emoji buttons in menus
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -94,6 +126,8 @@ and this project adheres to
|
||||
- ✨unify tab focus style for better visual consistency #1341
|
||||
- ♿hide decorative icons, label menus, avoid accessible name… #1362
|
||||
- ♻️(tilt) use helm dev-backend chart
|
||||
- 🩹(frontend) on main pages do not display leading emoji as page icon #1381
|
||||
- 🩹(frontend) handle properly emojis in interlinking #1381
|
||||
|
||||
### Removed
|
||||
|
||||
@@ -809,7 +843,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.8.2...main
|
||||
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.9.0...main
|
||||
[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
|
||||
[v3.8.0]: https://github.com/suitenumerique/docs/releases/v3.8.0
|
||||
|
||||
@@ -213,6 +213,7 @@ logs: ## display app-dev logs (follow mode)
|
||||
.PHONY: logs
|
||||
|
||||
run-backend: ## Start only the backend application and all needed services
|
||||
@$(COMPOSE) up --force-recreate -d docspec
|
||||
@$(COMPOSE) up --force-recreate -d celery-dev
|
||||
@$(COMPOSE) up --force-recreate -d y-provider-development
|
||||
@$(COMPOSE) up --force-recreate -d nginx
|
||||
|
||||
@@ -217,3 +217,8 @@ services:
|
||||
kc_postgresql:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
|
||||
docspec:
|
||||
image: ghcr.io/docspecio/api:2.0.2
|
||||
ports:
|
||||
- "4000:4000"
|
||||
@@ -103,6 +103,7 @@ These are the environment variables you can set for the `impress-backend` contai
|
||||
| USER_OIDC_ESSENTIAL_CLAIMS | Essential claims in OIDC token | [] |
|
||||
| Y_PROVIDER_API_BASE_URL | Y Provider url | |
|
||||
| Y_PROVIDER_API_KEY | Y provider API key | |
|
||||
| DOCSPEC_API_URL | URL to endpoint of DocSpec conversion API | |
|
||||
|
||||
|
||||
## impress-frontend image
|
||||
|
||||
@@ -67,5 +67,7 @@ DJANGO_SERVER_TO_SERVER_API_TOKENS=server-api-token
|
||||
Y_PROVIDER_API_BASE_URL=http://y-provider-development:4444/api/
|
||||
Y_PROVIDER_API_KEY=yprovider-api-key
|
||||
|
||||
DOCSPEC_API_URL=http://docspec:4000/conversion
|
||||
|
||||
# Theme customization
|
||||
THEME_CUSTOMIZATION_CACHE_TIMEOUT=15
|
||||
@@ -3,6 +3,7 @@ BURST_THROTTLE_RATES="200/minute"
|
||||
COLLABORATION_API_URL=http://y-provider:4444/collaboration/api/
|
||||
SUSTAINED_THROTTLE_RATES="200/hour"
|
||||
Y_PROVIDER_API_BASE_URL=http://y-provider:4444/api/
|
||||
DOCSPEC_API_URL=http://docspec:4000/conversion
|
||||
|
||||
# Throttle
|
||||
API_DOCUMENT_THROTTLE_RATE=1000/min
|
||||
|
||||
+6
-2
@@ -19,13 +19,17 @@
|
||||
"matchPackageNames": ["redis"],
|
||||
"allowedVersions": "<6.0.0"
|
||||
},
|
||||
{
|
||||
"groupName": "allowed pylint versions",
|
||||
"matchManagers": ["pep621"],
|
||||
"matchPackageNames": ["pylint"],
|
||||
"allowedVersions": "<4.0.0"
|
||||
},
|
||||
{
|
||||
"enabled": false,
|
||||
"groupName": "ignored js dependencies",
|
||||
"matchManagers": ["npm"],
|
||||
"matchPackageNames": [
|
||||
"@hocuspocus/provider",
|
||||
"@hocuspocus/server",
|
||||
"docx",
|
||||
"fetch-mock",
|
||||
"node",
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.utils.functional import lazy
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.services import mime_types
|
||||
import magic
|
||||
from rest_framework import serializers
|
||||
|
||||
@@ -17,7 +18,7 @@ from core import choices, enums, models, utils, validators
|
||||
from core.services.ai_services import AI_ACTIONS
|
||||
from core.services.converter_services import (
|
||||
ConversionError,
|
||||
YdocConverter,
|
||||
Converter,
|
||||
)
|
||||
|
||||
|
||||
@@ -187,6 +188,7 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
|
||||
content = serializers.CharField(required=False)
|
||||
websocket = serializers.BooleanField(required=False, write_only=True)
|
||||
file = serializers.FileField(required=False, write_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Document
|
||||
@@ -203,6 +205,7 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
"deleted_at",
|
||||
"depth",
|
||||
"excerpt",
|
||||
"file",
|
||||
"is_favorite",
|
||||
"link_role",
|
||||
"link_reach",
|
||||
@@ -460,7 +463,11 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
|
||||
language = user.language or language
|
||||
|
||||
try:
|
||||
document_content = YdocConverter().convert(validated_data["content"])
|
||||
document_content = Converter().convert(
|
||||
validated_data["content"],
|
||||
mime_types.MARKDOWN,
|
||||
mime_types.YJS
|
||||
)
|
||||
except ConversionError as err:
|
||||
raise serializers.ValidationError(
|
||||
{"content": ["Could not convert content"]}
|
||||
|
||||
@@ -39,14 +39,12 @@ from core import authentication, choices, enums, models
|
||||
from core.services.ai_services import AIService
|
||||
from core.services.collaboration_services import CollaborationService
|
||||
from core.services.converter_services import (
|
||||
ConversionError,
|
||||
ServiceUnavailableError as YProviderServiceUnavailableError,
|
||||
)
|
||||
from core.services.converter_services import (
|
||||
ValidationError as YProviderValidationError,
|
||||
Converter,
|
||||
)
|
||||
from core.services.converter_services import (
|
||||
YdocConverter,
|
||||
)
|
||||
from core.services import mime_types
|
||||
from core.tasks.mail import send_ask_for_access_mail
|
||||
from core.utils import extract_attachments, filter_descendants
|
||||
|
||||
@@ -503,6 +501,27 @@ class DocumentViewSet(
|
||||
"IN SHARE ROW EXCLUSIVE MODE;"
|
||||
)
|
||||
|
||||
# Remove file from validated_data as it's not a model field
|
||||
# Process it if present
|
||||
uploaded_file = serializer.validated_data.pop("file", None)
|
||||
|
||||
# If a file is uploaded, convert it to Yjs format and set as content
|
||||
if uploaded_file:
|
||||
try:
|
||||
file_content = uploaded_file.read()
|
||||
|
||||
converter = Converter()
|
||||
converted_content = converter.convert(
|
||||
file_content,
|
||||
content_type=uploaded_file.content_type,
|
||||
accept=mime_types.YJS
|
||||
)
|
||||
serializer.validated_data["content"] = converted_content
|
||||
except ConversionError as err:
|
||||
raise drf.exceptions.ValidationError(
|
||||
{"file": ["Could not convert file content"]}
|
||||
) from err
|
||||
|
||||
obj = models.Document.add_root(
|
||||
creator=self.request.user,
|
||||
**serializer.validated_data,
|
||||
@@ -636,6 +655,9 @@ class DocumentViewSet(
|
||||
.values_list("document__path", flat=True)
|
||||
)
|
||||
|
||||
if not access_documents_paths:
|
||||
return self.get_response_for_queryset(self.queryset.none())
|
||||
|
||||
children_clause = db.Q()
|
||||
for path in access_documents_paths:
|
||||
children_clause |= db.Q(path__startswith=path)
|
||||
@@ -1102,7 +1124,7 @@ class DocumentViewSet(
|
||||
@drf.decorators.action(
|
||||
detail=True,
|
||||
methods=["get", "delete"],
|
||||
url_path="versions/(?P<version_id>[0-9a-z-]+)",
|
||||
url_path=r"versions/(?P<version_id>[A-Za-z0-9._+\-=~]{1,1024})",
|
||||
)
|
||||
# pylint: disable=unused-argument
|
||||
def versions_detail(self, request, pk, version_id, *args, **kwargs):
|
||||
@@ -1599,14 +1621,14 @@ class DocumentViewSet(
|
||||
if base64_content is not None:
|
||||
# Convert using the y-provider service
|
||||
try:
|
||||
yprovider = YdocConverter()
|
||||
yprovider = Converter()
|
||||
result = yprovider.convert(
|
||||
base64.b64decode(base64_content),
|
||||
"application/vnd.yjs.doc",
|
||||
mime_types.YJS,
|
||||
{
|
||||
"markdown": "text/markdown",
|
||||
"html": "text/html",
|
||||
"json": "application/json",
|
||||
"markdown": mime_types.MARKDOWN,
|
||||
"html": mime_types.HTML,
|
||||
"json": mime_types.JSON,
|
||||
}[content_format],
|
||||
)
|
||||
content = result
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-22 06:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("core", "0024_add_is_masked_field_to_link_trace"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="short_name",
|
||||
field=models.CharField(
|
||||
blank=True, max_length=100, null=True, verbose_name="short name"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -148,7 +148,9 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
)
|
||||
|
||||
full_name = models.CharField(_("full name"), max_length=100, null=True, blank=True)
|
||||
short_name = models.CharField(_("short name"), max_length=20, null=True, blank=True)
|
||||
short_name = models.CharField(
|
||||
_("short name"), max_length=100, null=True, blank=True
|
||||
)
|
||||
|
||||
email = models.EmailField(_("identity email address"), blank=True, null=True)
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ from base64 import b64encode
|
||||
from django.conf import settings
|
||||
|
||||
import requests
|
||||
import typing
|
||||
|
||||
from core.services import mime_types
|
||||
|
||||
class ConversionError(Exception):
|
||||
"""Base exception for conversion-related errors."""
|
||||
@@ -19,8 +21,65 @@ class ServiceUnavailableError(ConversionError):
|
||||
"""Raised when the conversion service is unavailable."""
|
||||
|
||||
|
||||
class ConverterProtocol(typing.Protocol):
|
||||
def convert(self, text, content_type, accept): ...
|
||||
|
||||
|
||||
class Converter:
|
||||
docspec: ConverterProtocol
|
||||
ydoc: ConverterProtocol
|
||||
|
||||
def __init__(self):
|
||||
self.docspec = DocSpecConverter()
|
||||
self.ydoc = YdocConverter()
|
||||
|
||||
def convert(self, input, content_type, accept):
|
||||
"""Convert input into other formats using external microservices."""
|
||||
|
||||
if content_type == mime_types.DOCX and accept == mime_types.YJS:
|
||||
return self.convert(
|
||||
self.docspec.convert(input, mime_types.DOCX, mime_types.BLOCKNOTE),
|
||||
mime_types.BLOCKNOTE,
|
||||
mime_types.YJS
|
||||
)
|
||||
|
||||
return self.ydoc.convert(input, content_type, accept)
|
||||
|
||||
|
||||
class DocSpecConverter:
|
||||
"""Service class for DocSpec conversion-related operations."""
|
||||
|
||||
def _request(self, url, data, content_type):
|
||||
"""Make a request to the DocSpec API."""
|
||||
|
||||
response = requests.post(
|
||||
url,
|
||||
headers={"Accept": mime_types.BLOCKNOTE},
|
||||
files={"file": ("document.docx", data, content_type)},
|
||||
timeout=settings.CONVERSION_API_TIMEOUT,
|
||||
verify=settings.CONVERSION_API_SECURE,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
def convert(self, data, content_type, accept):
|
||||
"""Convert a Document to BlockNote."""
|
||||
if not data:
|
||||
raise ValidationError("Input data cannot be empty")
|
||||
|
||||
if content_type != mime_types.DOCX or accept != mime_types.BLOCKNOTE:
|
||||
raise ValidationError(f"Conversion from {content_type} to {accept} is not supported.")
|
||||
|
||||
try:
|
||||
return self._request(settings.DOCSPEC_API_URL, data, content_type).content
|
||||
except requests.RequestException as err:
|
||||
raise ServiceUnavailableError(
|
||||
"Failed to connect to DocSpec conversion service",
|
||||
) from err
|
||||
|
||||
|
||||
class YdocConverter:
|
||||
"""Service class for conversion-related operations."""
|
||||
"""Service class for YDoc conversion-related operations."""
|
||||
|
||||
@property
|
||||
def auth_header(self):
|
||||
@@ -45,7 +104,7 @@ class YdocConverter:
|
||||
return response
|
||||
|
||||
def convert(
|
||||
self, text, content_type="text/markdown", accept="application/vnd.yjs.doc"
|
||||
self, text, content_type=mime_types.MARKDOWN, accept=mime_types.YJS
|
||||
):
|
||||
"""Convert a Markdown text into our internal format using an external microservice."""
|
||||
|
||||
@@ -59,14 +118,14 @@ class YdocConverter:
|
||||
content_type,
|
||||
accept,
|
||||
)
|
||||
if accept == "application/vnd.yjs.doc":
|
||||
if accept == mime_types.YJS:
|
||||
return b64encode(response.content).decode("utf-8")
|
||||
if accept in {"text/markdown", "text/html"}:
|
||||
if accept in {mime_types.MARKDOWN, "text/html"}:
|
||||
return response.text
|
||||
if accept == "application/json":
|
||||
if accept == mime_types.JSON:
|
||||
return response.json()
|
||||
raise ValidationError("Unsupported format")
|
||||
except requests.RequestException as err:
|
||||
raise ServiceUnavailableError(
|
||||
"Failed to connect to conversion service",
|
||||
f"Failed to connect to YDoc conversion service {content_type}, {accept}",
|
||||
) from err
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
BLOCKNOTE = "application/vnd.blocknote+json"
|
||||
YJS = "application/vnd.yjs.doc"
|
||||
MARKDOWN = "text/markdown"
|
||||
JSON = "application/json"
|
||||
DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
HTML = "text/html"
|
||||
@@ -57,7 +57,7 @@ def test_authentication_getter_existing_user_via_email(
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
with django_assert_num_queries(3): # user by sub, user by mail, update sub
|
||||
with django_assert_num_queries(4): # user by sub, user by mail, update sub
|
||||
user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
@@ -214,7 +214,7 @@ def test_authentication_getter_existing_user_change_fields_sub(
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
# One and only one additional update query when a field has changed
|
||||
with django_assert_num_queries(2):
|
||||
with django_assert_num_queries(3):
|
||||
authenticated_user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
@@ -256,7 +256,7 @@ def test_authentication_getter_existing_user_change_fields_email(
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
# One and only one additional update query when a field has changed
|
||||
with django_assert_num_queries(3):
|
||||
with django_assert_num_queries(4):
|
||||
authenticated_user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
|
||||
@@ -293,3 +293,29 @@ def test_api_documents_trashbin_distinct():
|
||||
content = response.json()
|
||||
assert len(content["results"]) == 1
|
||||
assert content["results"][0]["id"] == str(document.id)
|
||||
|
||||
|
||||
def test_api_documents_trashbin_empty_queryset_bug():
|
||||
"""
|
||||
Test that users with no owner role don't see documents.
|
||||
"""
|
||||
# Create a new user with no owner access to any document
|
||||
new_user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(new_user)
|
||||
|
||||
# Create some deleted documents owned by other users
|
||||
other_user = factories.UserFactory()
|
||||
item1 = factories.DocumentFactory(users=[(other_user, "owner")])
|
||||
item1.soft_delete()
|
||||
item2 = factories.DocumentFactory(users=[(other_user, "owner")])
|
||||
item2.soft_delete()
|
||||
item3 = factories.DocumentFactory(users=[(other_user, "owner")])
|
||||
item3.soft_delete()
|
||||
|
||||
response = client.get("/api/v1.0/documents/trashbin/")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert content["count"] == 0
|
||||
assert len(content["results"]) == 0
|
||||
|
||||
@@ -680,6 +680,12 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# DocSpec API microservice
|
||||
DOCSPEC_API_URL = values.Value(
|
||||
environ_name="DOCSPEC_API_URL",
|
||||
environ_prefix=None
|
||||
)
|
||||
|
||||
# Conversion endpoint
|
||||
CONVERSION_API_ENDPOINT = values.Value(
|
||||
default="convert",
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
|
||||
"PO-Revision-Date: 2025-11-10 09:54\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Breton\n"
|
||||
"Language: br_FR\n"
|
||||
@@ -17,20 +17,20 @@ msgstr ""
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 18\n"
|
||||
|
||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
||||
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||
msgid "Personal info"
|
||||
msgstr "Titouroù personel"
|
||||
|
||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
||||
#: core/admin.py:138
|
||||
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||
#: core/admin.py:137
|
||||
msgid "Permissions"
|
||||
msgstr "Aotreoù"
|
||||
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||
msgid "Important dates"
|
||||
msgstr "Deiziadoù a-bouez"
|
||||
|
||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
||||
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||
msgid "Tree structure"
|
||||
msgstr "Gwezennadur"
|
||||
|
||||
@@ -79,7 +79,7 @@ msgstr "Doare korf"
|
||||
msgid "Format"
|
||||
msgstr "Stumm"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "eilenn {title}"
|
||||
@@ -178,228 +178,228 @@ msgstr ""
|
||||
msgid "full name"
|
||||
msgstr "anv klok"
|
||||
|
||||
#: build/lib/core/models.py:151 core/models.py:151
|
||||
#: build/lib/core/models.py:152 core/models.py:152
|
||||
msgid "short name"
|
||||
msgstr "anv berr"
|
||||
|
||||
#: build/lib/core/models.py:153 core/models.py:153
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr "postel identelezh"
|
||||
|
||||
#: build/lib/core/models.py:158 core/models.py:158
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr "postel ar merour"
|
||||
|
||||
#: build/lib/core/models.py:165 core/models.py:165
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "language"
|
||||
msgstr "yezh"
|
||||
|
||||
#: build/lib/core/models.py:166 core/models.py:166
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "Ar yezh a vo implijet evit etrefas an implijer."
|
||||
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
#: build/lib/core/models.py:176 core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Ar gwerzhid-eur a vo implijet evit etrefas an implijer."
|
||||
|
||||
#: build/lib/core/models.py:177 core/models.py:177
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "trevnad"
|
||||
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
#: build/lib/core/models.py:181 core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Pe vefe an implijer un aparailh pe un implijer gwirion."
|
||||
|
||||
#: build/lib/core/models.py:182 core/models.py:182
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "statud ar skipailh"
|
||||
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Ma c'hall an implijer kevreañ ouzh al lec'hienn verañ-mañ."
|
||||
|
||||
#: build/lib/core/models.py:187 core/models.py:187
|
||||
#: build/lib/core/models.py:189 core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "oberiant"
|
||||
|
||||
#: build/lib/core/models.py:190 core/models.py:190
|
||||
#: build/lib/core/models.py:192 core/models.py:192
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Ma rank bezañ tretet an implijer-mañ evel oberiant. Diziuzit an dra-mañ e-plas dilemel kontoù."
|
||||
|
||||
#: build/lib/core/models.py:202 core/models.py:202
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
msgid "user"
|
||||
msgstr "implijer"
|
||||
|
||||
#: build/lib/core/models.py:203 core/models.py:203
|
||||
#: build/lib/core/models.py:205 core/models.py:205
|
||||
msgid "users"
|
||||
msgstr "implijerien"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
|
||||
#: core/models.py:361 core/models.py:1284
|
||||
msgid "title"
|
||||
msgstr "titl"
|
||||
|
||||
#: build/lib/core/models.py:360 core/models.py:360
|
||||
#: build/lib/core/models.py:362 core/models.py:362
|
||||
msgid "excerpt"
|
||||
msgstr "bomm"
|
||||
|
||||
#: build/lib/core/models.py:409 core/models.py:409
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Document"
|
||||
msgstr "Restr"
|
||||
|
||||
#: build/lib/core/models.py:410 core/models.py:410
|
||||
#: build/lib/core/models.py:412 core/models.py:412
|
||||
msgid "Documents"
|
||||
msgstr "Restroù"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||
#: core/models.py:822
|
||||
msgid "Untitled Document"
|
||||
msgstr "Restr hep titl"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:857 core/models.py:857
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} en deus rannet ur restr ganeoc'h!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:861 core/models.py:861
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} en deus pedet ac'hanoc'h gant ar rol \"{role}\" war ar restr da-heul:"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:867 core/models.py:867
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} en deus rannet ur restr ganeoc'h: {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Roud liamm ar restr/an implijer"
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:968 core/models.py:968
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Roudoù liamm ar restr/an implijer"
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Ur roud liamm a zo dija evit an restr/an implijer."
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorite"
|
||||
msgstr "Restr muiañ-karet"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:998 core/models.py:998
|
||||
msgid "Document favorites"
|
||||
msgstr "Restroù muiañ-karet"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Ar restr-mañ a zo ur restr muiañ karet gant an implijer-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relation"
|
||||
msgstr "Liamm restr/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||
msgid "Document/user relations"
|
||||
msgstr "Liammoù restr/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "This user is already in this document."
|
||||
msgstr "An implijer-mañ a zo dija er restr-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
|
||||
#: core/models.py:1045 core/models.py:1370
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "An implijer pe ar skipailh a rank bezañ termenet, ket an daou avat."
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for access"
|
||||
msgstr "Goulenn tizhout ar restr"
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Goulennoù tizhout ar restr"
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
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:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1263 core/models.py:1263
|
||||
#, 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:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1267 core/models.py:1267
|
||||
#, 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:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1273 core/models.py:1273
|
||||
#, 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:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "description"
|
||||
msgstr "deskrivadur"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "code"
|
||||
msgstr "kod"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "public"
|
||||
msgstr "publik"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: build/lib/core/models.py:1291 core/models.py:1291
|
||||
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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Template"
|
||||
msgstr "Patrom"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
msgid "Templates"
|
||||
msgstr "Patromoù"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relation"
|
||||
msgstr "Liamm patrom/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1352 core/models.py:1352
|
||||
msgid "Template/user relations"
|
||||
msgstr "Liammoù patrom/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1358 core/models.py:1358
|
||||
msgid "This user is already in this template."
|
||||
msgstr "An implijer-mañ a zo dija er patrom-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1364 core/models.py:1364
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "email address"
|
||||
msgstr "postel"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitation"
|
||||
msgstr "Pedadenn d'ur restr"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1461 core/models.py:1461
|
||||
msgid "Document invitations"
|
||||
msgstr "Pedadennoù d'ur restr"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1481 core/models.py:1481
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
|
||||
"PO-Revision-Date: 2025-11-10 09:54\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -17,20 +17,20 @@ msgstr ""
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 18\n"
|
||||
|
||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
||||
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||
msgid "Personal info"
|
||||
msgstr "Persönliche Daten"
|
||||
|
||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
||||
#: core/admin.py:138
|
||||
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||
#: core/admin.py:137
|
||||
msgid "Permissions"
|
||||
msgstr "Berechtigungen"
|
||||
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||
msgid "Important dates"
|
||||
msgstr "Wichtige Daten"
|
||||
|
||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
||||
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||
msgid "Tree structure"
|
||||
msgstr "Baumstruktur"
|
||||
|
||||
@@ -79,7 +79,7 @@ msgstr "Typ"
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "Kopie von {title}"
|
||||
@@ -178,228 +178,228 @@ msgstr ""
|
||||
msgid "full name"
|
||||
msgstr "Name"
|
||||
|
||||
#: build/lib/core/models.py:151 core/models.py:151
|
||||
#: build/lib/core/models.py:152 core/models.py:152
|
||||
msgid "short name"
|
||||
msgstr "Kurzbezeichnung"
|
||||
|
||||
#: build/lib/core/models.py:153 core/models.py:153
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr "Identitäts-E-Mail-Adresse"
|
||||
|
||||
#: build/lib/core/models.py:158 core/models.py:158
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr "Admin E-Mail-Adresse"
|
||||
|
||||
#: build/lib/core/models.py:165 core/models.py:165
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "language"
|
||||
msgstr "Sprache"
|
||||
|
||||
#: build/lib/core/models.py:166 core/models.py:166
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "Die Sprache, in der der Benutzer die Benutzeroberfläche sehen möchte."
|
||||
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
#: build/lib/core/models.py:176 core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Die Zeitzone, in der der Nutzer Zeiten sehen möchte."
|
||||
|
||||
#: build/lib/core/models.py:177 core/models.py:177
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "Gerät"
|
||||
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
#: build/lib/core/models.py:181 core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Ob der Benutzer ein Gerät oder ein echter Benutzer ist."
|
||||
|
||||
#: build/lib/core/models.py:182 core/models.py:182
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "Status des Teammitgliedes"
|
||||
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Gibt an, ob der Benutzer sich in diese Admin-Seite einloggen kann."
|
||||
|
||||
#: build/lib/core/models.py:187 core/models.py:187
|
||||
#: build/lib/core/models.py:189 core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "aktiviert"
|
||||
|
||||
#: build/lib/core/models.py:190 core/models.py:190
|
||||
#: build/lib/core/models.py:192 core/models.py:192
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Ob dieser Benutzer als aktiviert behandelt werden soll. Deaktivieren Sie diese Option, anstatt Konten zu löschen."
|
||||
|
||||
#: build/lib/core/models.py:202 core/models.py:202
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
msgid "user"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: build/lib/core/models.py:203 core/models.py:203
|
||||
#: build/lib/core/models.py:205 core/models.py:205
|
||||
msgid "users"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
|
||||
#: core/models.py:361 core/models.py:1284
|
||||
msgid "title"
|
||||
msgstr "Titel"
|
||||
|
||||
#: build/lib/core/models.py:360 core/models.py:360
|
||||
#: build/lib/core/models.py:362 core/models.py:362
|
||||
msgid "excerpt"
|
||||
msgstr "Auszug"
|
||||
|
||||
#: build/lib/core/models.py:409 core/models.py:409
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Document"
|
||||
msgstr "Dokument"
|
||||
|
||||
#: build/lib/core/models.py:410 core/models.py:410
|
||||
#: build/lib/core/models.py:412 core/models.py:412
|
||||
msgid "Documents"
|
||||
msgstr "Dokumente"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||
#: core/models.py:822
|
||||
msgid "Untitled Document"
|
||||
msgstr "Unbenanntes Dokument"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:857 core/models.py:857
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:861 core/models.py:861
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} hat Sie mit der Rolle \"{role}\" zu folgendem Dokument eingeladen:"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:867 core/models.py:867
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} hat ein Dokument mit Ihnen geteilt: {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:968 core/models.py:968
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Für dieses Dokument/ diesen Benutzer ist bereits eine Linkverfolgung vorhanden."
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorite"
|
||||
msgstr "Dokumentenfavorit"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:998 core/models.py:998
|
||||
msgid "Document favorites"
|
||||
msgstr "Dokumentfavoriten"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Dieses Dokument ist bereits durch den gleichen Benutzer favorisiert worden."
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relation"
|
||||
msgstr "Dokument/Benutzerbeziehung"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||
msgid "Document/user relations"
|
||||
msgstr "Dokument/Benutzerbeziehungen"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
|
||||
#: core/models.py:1045 core/models.py:1370
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Benutzer oder Team müssen gesetzt werden, nicht beides."
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1263 core/models.py:1263
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1267 core/models.py:1267
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1273 core/models.py:1273
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "description"
|
||||
msgstr "Beschreibung"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "code"
|
||||
msgstr "Code"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "css"
|
||||
msgstr "CSS"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "public"
|
||||
msgstr "öffentlich"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: build/lib/core/models.py:1291 core/models.py:1291
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Template"
|
||||
msgstr "Vorlage"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
msgid "Templates"
|
||||
msgstr "Vorlagen"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relation"
|
||||
msgstr "Vorlage/Benutzer-Beziehung"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1352 core/models.py:1352
|
||||
msgid "Template/user relations"
|
||||
msgstr "Vorlage/Benutzerbeziehungen"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1358 core/models.py:1358
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1364 core/models.py:1364
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Dieses Team ist bereits in diesem Template."
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "email address"
|
||||
msgstr "E-Mail-Adresse"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitation"
|
||||
msgstr "Einladung zum Dokument"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1461 core/models.py:1461
|
||||
msgid "Document invitations"
|
||||
msgstr "Dokumenteinladungen"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1481 core/models.py:1481
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
|
||||
"PO-Revision-Date: 2025-11-10 09:54\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_US\n"
|
||||
@@ -17,20 +17,20 @@ msgstr ""
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 18\n"
|
||||
|
||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
||||
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||
msgid "Personal info"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
||||
#: core/admin.py:138
|
||||
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||
#: core/admin.py:137
|
||||
msgid "Permissions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||
msgid "Important dates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
||||
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||
msgid "Tree structure"
|
||||
msgstr ""
|
||||
|
||||
@@ -79,7 +79,7 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
@@ -178,228 +178,228 @@ msgstr ""
|
||||
msgid "full name"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:151 core/models.py:151
|
||||
#: build/lib/core/models.py:152 core/models.py:152
|
||||
msgid "short name"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:153 core/models.py:153
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:158 core/models.py:158
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:165 core/models.py:165
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "language"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:166 core/models.py:166
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
#: build/lib/core/models.py:176 core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:177 core/models.py:177
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
msgid "device"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
#: build/lib/core/models.py:181 core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:182 core/models.py:182
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:187 core/models.py:187
|
||||
#: build/lib/core/models.py:189 core/models.py:189
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:190 core/models.py:190
|
||||
#: build/lib/core/models.py:192 core/models.py:192
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:202 core/models.py:202
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:203 core/models.py:203
|
||||
#: build/lib/core/models.py:205 core/models.py:205
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
|
||||
#: core/models.py:361 core/models.py:1284
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:360 core/models.py:360
|
||||
#: build/lib/core/models.py:362 core/models.py:362
|
||||
msgid "excerpt"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:409 core/models.py:409
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:410 core/models.py:410
|
||||
#: build/lib/core/models.py:412 core/models.py:412
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||
#: core/models.py:822
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:857 core/models.py:857
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:861 core/models.py:861
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:867 core/models.py:867
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:968 core/models.py:968
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:998 core/models.py:998
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
|
||||
#: core/models.py:1045 core/models.py:1370
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1263 core/models.py:1263
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1267 core/models.py:1267
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1273 core/models.py:1273
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: build/lib/core/models.py:1291 core/models.py:1291
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1352 core/models.py:1352
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1358 core/models.py:1358
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1364 core/models.py:1364
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1461 core/models.py:1461
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1481 core/models.py:1481
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
|
||||
"PO-Revision-Date: 2025-11-10 09:54\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Language: es_ES\n"
|
||||
@@ -17,20 +17,20 @@ msgstr ""
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 18\n"
|
||||
|
||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
||||
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||
msgid "Personal info"
|
||||
msgstr "Información Personal"
|
||||
|
||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
||||
#: core/admin.py:138
|
||||
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||
#: core/admin.py:137
|
||||
msgid "Permissions"
|
||||
msgstr "Permisos"
|
||||
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||
msgid "Important dates"
|
||||
msgstr "Fechas importantes"
|
||||
|
||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
||||
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||
msgid "Tree structure"
|
||||
msgstr "Estructura en árbol"
|
||||
|
||||
@@ -79,7 +79,7 @@ msgstr "Tipo de Cuerpo"
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copia de {title}"
|
||||
@@ -178,228 +178,228 @@ msgstr "Obligatorio. 255 caracteres o menos. Solo caracteres ASCII."
|
||||
msgid "full name"
|
||||
msgstr "nombre completo"
|
||||
|
||||
#: build/lib/core/models.py:151 core/models.py:151
|
||||
#: build/lib/core/models.py:152 core/models.py:152
|
||||
msgid "short name"
|
||||
msgstr "nombre abreviado"
|
||||
|
||||
#: build/lib/core/models.py:153 core/models.py:153
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr "correo electrónico de identidad"
|
||||
|
||||
#: build/lib/core/models.py:158 core/models.py:158
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr "correo electrónico del administrador"
|
||||
|
||||
#: build/lib/core/models.py:165 core/models.py:165
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "language"
|
||||
msgstr "idioma"
|
||||
|
||||
#: build/lib/core/models.py:166 core/models.py:166
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "El idioma en el que el usuario desea ver la interfaz."
|
||||
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
#: build/lib/core/models.py:176 core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "La zona horaria en la que el usuario quiere ver los tiempos."
|
||||
|
||||
#: build/lib/core/models.py:177 core/models.py:177
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "dispositivo"
|
||||
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
#: build/lib/core/models.py:181 core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Si el usuario es un dispositivo o un usuario real."
|
||||
|
||||
#: build/lib/core/models.py:182 core/models.py:182
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "rol en el equipo"
|
||||
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Si el usuario puede iniciar sesión en esta página web de administración."
|
||||
|
||||
#: build/lib/core/models.py:187 core/models.py:187
|
||||
#: build/lib/core/models.py:189 core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "activo"
|
||||
|
||||
#: build/lib/core/models.py:190 core/models.py:190
|
||||
#: build/lib/core/models.py:192 core/models.py:192
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Si este usuario debe ser considerado como activo. Deseleccionar en lugar de eliminar cuentas."
|
||||
|
||||
#: build/lib/core/models.py:202 core/models.py:202
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
msgid "user"
|
||||
msgstr "usuario"
|
||||
|
||||
#: build/lib/core/models.py:203 core/models.py:203
|
||||
#: build/lib/core/models.py:205 core/models.py:205
|
||||
msgid "users"
|
||||
msgstr "usuarios"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
|
||||
#: core/models.py:361 core/models.py:1284
|
||||
msgid "title"
|
||||
msgstr "título"
|
||||
|
||||
#: build/lib/core/models.py:360 core/models.py:360
|
||||
#: build/lib/core/models.py:362 core/models.py:362
|
||||
msgid "excerpt"
|
||||
msgstr "resumen"
|
||||
|
||||
#: build/lib/core/models.py:409 core/models.py:409
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Document"
|
||||
msgstr "Documento"
|
||||
|
||||
#: build/lib/core/models.py:410 core/models.py:410
|
||||
#: build/lib/core/models.py:412 core/models.py:412
|
||||
msgid "Documents"
|
||||
msgstr "Documentos"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||
#: core/models.py:822
|
||||
msgid "Untitled Document"
|
||||
msgstr "Documento sin título"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:857 core/models.py:857
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "¡{name} ha compartido un documento contigo!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:861 core/models.py:861
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "Te ha invitado {name} al siguiente documento con el rol \"{role}\" :"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:867 core/models.py:867
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} ha compartido un documento contigo: {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Traza del enlace de documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:968 core/models.py:968
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Trazas del enlace de documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Ya existe una traza de enlace para este documento/usuario."
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorite"
|
||||
msgstr "Documento favorito"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:998 core/models.py:998
|
||||
msgid "Document favorites"
|
||||
msgstr "Documentos favoritos"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Este documento ya ha sido marcado como favorito por el usuario."
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relation"
|
||||
msgstr "Relación documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||
msgid "Document/user relations"
|
||||
msgstr "Relaciones documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Este usuario ya forma parte del documento."
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Este equipo ya forma parte del documento."
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
|
||||
#: core/models.py:1045 core/models.py:1370
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Debe establecerse un usuario o un equipo, no ambos."
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for access"
|
||||
msgstr "Solicitud de acceso"
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Solicitud de accesos"
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
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:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1263 core/models.py:1263
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "¡{name} desea acceder a un documento!"
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1267 core/models.py:1267
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} desea acceso al siguiente documento:"
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1273 core/models.py:1273
|
||||
#, 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:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "description"
|
||||
msgstr "descripción"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "code"
|
||||
msgstr "código"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "public"
|
||||
msgstr "público"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: build/lib/core/models.py:1291 core/models.py:1291
|
||||
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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Template"
|
||||
msgstr "Plantilla"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
msgid "Templates"
|
||||
msgstr "Plantillas"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relation"
|
||||
msgstr "Relación plantilla/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1352 core/models.py:1352
|
||||
msgid "Template/user relations"
|
||||
msgstr "Relaciones plantilla/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1358 core/models.py:1358
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Este usuario ya forma parte de la plantilla."
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1364 core/models.py:1364
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Este equipo ya se encuentra en esta plantilla."
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "email address"
|
||||
msgstr "dirección de correo electrónico"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitation"
|
||||
msgstr "Invitación al documento"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1461 core/models.py:1461
|
||||
msgid "Document invitations"
|
||||
msgstr "Invitaciones a documentos"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1481 core/models.py:1481
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Este correo electrónico está asociado a un usuario registrado."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
|
||||
"PO-Revision-Date: 2025-11-10 09:54\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr_FR\n"
|
||||
@@ -17,20 +17,20 @@ msgstr ""
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 18\n"
|
||||
|
||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
||||
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||
msgid "Personal info"
|
||||
msgstr "Infos Personnelles"
|
||||
|
||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
||||
#: core/admin.py:138
|
||||
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||
#: core/admin.py:137
|
||||
msgid "Permissions"
|
||||
msgstr "Permissions"
|
||||
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||
msgid "Important dates"
|
||||
msgstr "Dates importantes"
|
||||
|
||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
||||
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||
msgid "Tree structure"
|
||||
msgstr "Arborescence"
|
||||
|
||||
@@ -79,7 +79,7 @@ msgstr "Type de corps"
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copie de {title}"
|
||||
@@ -178,228 +178,228 @@ msgstr "Obligatoire. 255 caractères ou moins. Caractères ASCII uniquement."
|
||||
msgid "full name"
|
||||
msgstr "nom complet"
|
||||
|
||||
#: build/lib/core/models.py:151 core/models.py:151
|
||||
#: build/lib/core/models.py:152 core/models.py:152
|
||||
msgid "short name"
|
||||
msgstr "nom court"
|
||||
|
||||
#: build/lib/core/models.py:153 core/models.py:153
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr "adresse e-mail d'identité"
|
||||
|
||||
#: build/lib/core/models.py:158 core/models.py:158
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr "adresse e-mail de l'administrateur"
|
||||
|
||||
#: build/lib/core/models.py:165 core/models.py:165
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "language"
|
||||
msgstr "langue"
|
||||
|
||||
#: build/lib/core/models.py:166 core/models.py:166
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "La langue dans laquelle l'utilisateur veut voir l'interface."
|
||||
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
#: build/lib/core/models.py:176 core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
|
||||
|
||||
#: build/lib/core/models.py:177 core/models.py:177
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "appareil"
|
||||
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
#: build/lib/core/models.py:181 core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
|
||||
|
||||
#: build/lib/core/models.py:182 core/models.py:182
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "statut d'équipe"
|
||||
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
|
||||
|
||||
#: build/lib/core/models.py:187 core/models.py:187
|
||||
#: build/lib/core/models.py:189 core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "actif"
|
||||
|
||||
#: build/lib/core/models.py:190 core/models.py:190
|
||||
#: build/lib/core/models.py:192 core/models.py:192
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Si cet utilisateur doit être traité comme actif. Désélectionnez ceci au lieu de supprimer des comptes."
|
||||
|
||||
#: build/lib/core/models.py:202 core/models.py:202
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
msgid "user"
|
||||
msgstr "utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:203 core/models.py:203
|
||||
#: build/lib/core/models.py:205 core/models.py:205
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
|
||||
#: core/models.py:361 core/models.py:1284
|
||||
msgid "title"
|
||||
msgstr "titre"
|
||||
|
||||
#: build/lib/core/models.py:360 core/models.py:360
|
||||
#: build/lib/core/models.py:362 core/models.py:362
|
||||
msgid "excerpt"
|
||||
msgstr "extrait"
|
||||
|
||||
#: build/lib/core/models.py:409 core/models.py:409
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Document"
|
||||
msgstr "Document"
|
||||
|
||||
#: build/lib/core/models.py:410 core/models.py:410
|
||||
#: build/lib/core/models.py:412 core/models.py:412
|
||||
msgid "Documents"
|
||||
msgstr "Documents"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||
#: core/models.py:822
|
||||
msgid "Untitled Document"
|
||||
msgstr "Document sans titre"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:857 core/models.py:857
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} a partagé un document avec vous!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:861 core/models.py:861
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} vous a invité avec le rôle \"{role}\" sur le document suivant :"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:867 core/models.py:867
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} a partagé un document avec vous : {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Trace du lien document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:968 core/models.py:968
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Traces du lien document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Une trace de lien existe déjà pour ce document/utilisateur."
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorite"
|
||||
msgstr "Document favori"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:998 core/models.py:998
|
||||
msgid "Document favorites"
|
||||
msgstr "Documents favoris"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Ce document est déjà un favori de cet utilisateur."
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relation"
|
||||
msgstr "Relation document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||
msgid "Document/user relations"
|
||||
msgstr "Relations document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Cet utilisateur est déjà dans ce document."
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Cette équipe est déjà dans ce document."
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
|
||||
#: core/models.py:1045 core/models.py:1370
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "L'utilisateur ou l'équipe doivent être définis, pas les deux."
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for access"
|
||||
msgstr "Demande d'accès au document"
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Demande d'accès au document"
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
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:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1263 core/models.py:1263
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} souhaiterait accéder au document suivant !"
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1267 core/models.py:1267
|
||||
#, 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:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1273 core/models.py:1273
|
||||
#, 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:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "description"
|
||||
msgstr "description"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "css"
|
||||
msgstr "CSS"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "public"
|
||||
msgstr "public"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: build/lib/core/models.py:1291 core/models.py:1291
|
||||
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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Template"
|
||||
msgstr "Modèle"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
msgid "Templates"
|
||||
msgstr "Modèles"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relation"
|
||||
msgstr "Relation modèle/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1352 core/models.py:1352
|
||||
msgid "Template/user relations"
|
||||
msgstr "Relations modèle/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1358 core/models.py:1358
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Cet utilisateur est déjà dans ce modèle."
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1364 core/models.py:1364
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Cette équipe est déjà modèle."
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "email address"
|
||||
msgstr "adresse e-mail"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitation"
|
||||
msgstr "Invitation à un document"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1461 core/models.py:1461
|
||||
msgid "Document invitations"
|
||||
msgstr "Invitations à un document"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1481 core/models.py:1481
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
|
||||
"PO-Revision-Date: 2025-11-10 09:54\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Italian\n"
|
||||
"Language: it_IT\n"
|
||||
@@ -17,20 +17,20 @@ msgstr ""
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 18\n"
|
||||
|
||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
||||
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||
msgid "Personal info"
|
||||
msgstr "Informazioni personali"
|
||||
|
||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
||||
#: core/admin.py:138
|
||||
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||
#: core/admin.py:137
|
||||
msgid "Permissions"
|
||||
msgstr "Permessi"
|
||||
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||
msgid "Important dates"
|
||||
msgstr "Date importanti"
|
||||
|
||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
||||
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||
msgid "Tree structure"
|
||||
msgstr "Struttura ad albero"
|
||||
|
||||
@@ -79,7 +79,7 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copia di {title}"
|
||||
@@ -178,228 +178,228 @@ msgstr ""
|
||||
msgid "full name"
|
||||
msgstr "nome completo"
|
||||
|
||||
#: build/lib/core/models.py:151 core/models.py:151
|
||||
#: build/lib/core/models.py:152 core/models.py:152
|
||||
msgid "short name"
|
||||
msgstr "nome"
|
||||
|
||||
#: build/lib/core/models.py:153 core/models.py:153
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr "indirizzo email di identità"
|
||||
|
||||
#: build/lib/core/models.py:158 core/models.py:158
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr "Indirizzo email dell'amministratore"
|
||||
|
||||
#: build/lib/core/models.py:165 core/models.py:165
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "language"
|
||||
msgstr "lingua"
|
||||
|
||||
#: build/lib/core/models.py:166 core/models.py:166
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "La lingua in cui l'utente vuole vedere l'interfaccia."
|
||||
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
#: build/lib/core/models.py:176 core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Il fuso orario in cui l'utente vuole vedere gli orari."
|
||||
|
||||
#: build/lib/core/models.py:177 core/models.py:177
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "dispositivo"
|
||||
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
#: build/lib/core/models.py:181 core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Se l'utente è un dispositivo o un utente reale."
|
||||
|
||||
#: build/lib/core/models.py:182 core/models.py:182
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "stato del personale"
|
||||
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Indica se l'utente può accedere a questo sito amministratore."
|
||||
|
||||
#: build/lib/core/models.py:187 core/models.py:187
|
||||
#: build/lib/core/models.py:189 core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "attivo"
|
||||
|
||||
#: build/lib/core/models.py:190 core/models.py:190
|
||||
#: build/lib/core/models.py:192 core/models.py:192
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Indica se questo utente deve essere trattato come attivo. Deseleziona invece di eliminare gli account."
|
||||
|
||||
#: build/lib/core/models.py:202 core/models.py:202
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
msgid "user"
|
||||
msgstr "utente"
|
||||
|
||||
#: build/lib/core/models.py:203 core/models.py:203
|
||||
#: build/lib/core/models.py:205 core/models.py:205
|
||||
msgid "users"
|
||||
msgstr "utenti"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
|
||||
#: core/models.py:361 core/models.py:1284
|
||||
msgid "title"
|
||||
msgstr "titolo"
|
||||
|
||||
#: build/lib/core/models.py:360 core/models.py:360
|
||||
#: build/lib/core/models.py:362 core/models.py:362
|
||||
msgid "excerpt"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:409 core/models.py:409
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Document"
|
||||
msgstr "Documento"
|
||||
|
||||
#: build/lib/core/models.py:410 core/models.py:410
|
||||
#: build/lib/core/models.py:412 core/models.py:412
|
||||
msgid "Documents"
|
||||
msgstr "Documenti"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||
#: core/models.py:822
|
||||
msgid "Untitled Document"
|
||||
msgstr "Documento senza titolo"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:857 core/models.py:857
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} ha condiviso un documento con te!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:861 core/models.py:861
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} ti ha invitato con il ruolo \"{role}\" nel seguente documento:"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:867 core/models.py:867
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} ha condiviso un documento con te: {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:968 core/models.py:968
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorite"
|
||||
msgstr "Documento preferito"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:998 core/models.py:998
|
||||
msgid "Document favorites"
|
||||
msgstr "Documenti preferiti"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Questo utente è già presente in questo documento."
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Questo team è già presente in questo documento."
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
|
||||
#: core/models.py:1045 core/models.py:1370
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1263 core/models.py:1263
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1267 core/models.py:1267
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1273 core/models.py:1273
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "description"
|
||||
msgstr "descrizione"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "public"
|
||||
msgstr "pubblico"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: build/lib/core/models.py:1291 core/models.py:1291
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Indica se questo modello è pubblico per chiunque."
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Template"
|
||||
msgstr "Modello"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
msgid "Templates"
|
||||
msgstr "Modelli"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1352 core/models.py:1352
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1358 core/models.py:1358
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Questo utente è già in questo modello."
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1364 core/models.py:1364
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Questo team è già in questo modello."
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "email address"
|
||||
msgstr "indirizzo e-mail"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitation"
|
||||
msgstr "Invito al documento"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1461 core/models.py:1461
|
||||
msgid "Document invitations"
|
||||
msgstr "Inviti al documento"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1481 core/models.py:1481
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Questa email è già associata a un utente registrato."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
|
||||
"PO-Revision-Date: 2025-11-10 09:54\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Language: nl_NL\n"
|
||||
@@ -17,22 +17,22 @@ msgstr ""
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 18\n"
|
||||
|
||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
||||
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||
msgid "Personal info"
|
||||
msgstr "Persoonlijke informatie"
|
||||
|
||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
||||
#: core/admin.py:138
|
||||
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||
#: core/admin.py:137
|
||||
msgid "Permissions"
|
||||
msgstr "Toestemmingen"
|
||||
msgstr "Machtigingen"
|
||||
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||
msgid "Important dates"
|
||||
msgstr "Belangrijke datums"
|
||||
msgstr "Belangrijke data"
|
||||
|
||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
||||
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||
msgid "Tree structure"
|
||||
msgstr "Document structuur"
|
||||
msgstr "Boomstructuur"
|
||||
|
||||
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
|
||||
msgid "Title"
|
||||
@@ -40,7 +40,7 @@ msgstr "Titel"
|
||||
|
||||
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
|
||||
msgid "Creator is me"
|
||||
msgstr "Ik ben Eigenaar"
|
||||
msgstr "Ik ben eigenaar"
|
||||
|
||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||
msgid "Masked"
|
||||
@@ -48,15 +48,15 @@ msgstr "Gemaskeerd"
|
||||
|
||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||
msgid "Favorite"
|
||||
msgstr "Favoriete"
|
||||
msgstr "Favoriet"
|
||||
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Een nieuw document was gecreëerd voor u!"
|
||||
msgstr "Een nieuw document is namens u gemaakt!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "U heeft eigenaarschap van een nieuw document:"
|
||||
msgstr "U heeft eigenaarschap van een nieuw document gekregen:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||
msgid "This field is required."
|
||||
@@ -79,7 +79,7 @@ msgstr "Text type"
|
||||
msgid "Format"
|
||||
msgstr "Formaat"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "kopie van {title}"
|
||||
@@ -92,11 +92,11 @@ msgstr "Lezer"
|
||||
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
|
||||
#: core/choices.py:43
|
||||
msgid "Editor"
|
||||
msgstr "Bewerker"
|
||||
msgstr "Redacteur"
|
||||
|
||||
#: build/lib/core/choices.py:44 core/choices.py:44
|
||||
msgid "Administrator"
|
||||
msgstr "Administrator"
|
||||
msgstr "Beheerder"
|
||||
|
||||
#: build/lib/core/choices.py:45 core/choices.py:45
|
||||
msgid "Owner"
|
||||
@@ -104,7 +104,7 @@ msgstr "Eigenaar"
|
||||
|
||||
#: build/lib/core/choices.py:56 core/choices.py:56
|
||||
msgid "Restricted"
|
||||
msgstr "Niet toegestaan"
|
||||
msgstr "Beperkt"
|
||||
|
||||
#: build/lib/core/choices.py:60 core/choices.py:60
|
||||
msgid "Authenticated"
|
||||
@@ -148,11 +148,11 @@ msgstr "primaire sleutel voor dossier als UUID"
|
||||
|
||||
#: build/lib/core/models.py:87 core/models.py:87
|
||||
msgid "created on"
|
||||
msgstr "gemaakt op"
|
||||
msgstr "gecreëerd op"
|
||||
|
||||
#: build/lib/core/models.py:88 core/models.py:88
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "datum en tijd wanneer dossier was gecreëerd"
|
||||
msgstr "datum en tijd waarop dossier is gecreeërd"
|
||||
|
||||
#: build/lib/core/models.py:93 core/models.py:93
|
||||
msgid "updated on"
|
||||
@@ -164,7 +164,7 @@ msgstr "datum en tijd waarop dossier laatst was gewijzigd"
|
||||
|
||||
#: build/lib/core/models.py:130 core/models.py:130
|
||||
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
|
||||
msgstr "Wij konden geen gebruiker vinden met deze id, maar de email is al geassocieerd met een geregistreerde gebruiker."
|
||||
msgstr "Wij konden geen gebruiker vinden met dit id, maar de email is al geassocieerd met een geregistreerde gebruiker."
|
||||
|
||||
#: build/lib/core/models.py:141 core/models.py:141
|
||||
msgid "sub"
|
||||
@@ -172,234 +172,234 @@ msgstr "id"
|
||||
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Required. 255 characters or fewer. ASCII characters only."
|
||||
msgstr "Vereist. Minder dan 255 ASCII tekens."
|
||||
msgstr "Vereist. 255 tekens of minder. Alleen ASCII tekens."
|
||||
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "full name"
|
||||
msgstr "volledige naam"
|
||||
|
||||
#: build/lib/core/models.py:151 core/models.py:151
|
||||
#: build/lib/core/models.py:152 core/models.py:152
|
||||
msgid "short name"
|
||||
msgstr "gebruikersnaam"
|
||||
|
||||
#: build/lib/core/models.py:153 core/models.py:153
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr "identiteit email adres"
|
||||
msgstr "identiteit emailadres"
|
||||
|
||||
#: build/lib/core/models.py:158 core/models.py:158
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr "admin email adres"
|
||||
msgstr "admin emailadres"
|
||||
|
||||
#: build/lib/core/models.py:165 core/models.py:165
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "language"
|
||||
msgstr "taal"
|
||||
|
||||
#: build/lib/core/models.py:166 core/models.py:166
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "De taal waarin de gebruiker de interface wilt zien."
|
||||
msgstr "De taal waarin de gebruiker de interface wil zien."
|
||||
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
#: build/lib/core/models.py:176 core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "De tijdzone waarin de gebruiker de tijden wilt zien."
|
||||
msgstr "De tijdzone waarin de gebruiker de tijden wil zien."
|
||||
|
||||
#: build/lib/core/models.py:177 core/models.py:177
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "apparaat"
|
||||
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
#: build/lib/core/models.py:181 core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Of de gebruiker een apparaat is of een echte gebruiker."
|
||||
|
||||
#: build/lib/core/models.py:182 core/models.py:182
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "beheerder status"
|
||||
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Of de gebruiker kan inloggen in het admin gedeelte."
|
||||
msgstr "Of de gebruiker kan inloggen in het beheer gedeelte."
|
||||
|
||||
#: build/lib/core/models.py:187 core/models.py:187
|
||||
#: build/lib/core/models.py:189 core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "actief"
|
||||
|
||||
#: build/lib/core/models.py:190 core/models.py:190
|
||||
#: build/lib/core/models.py:192 core/models.py:192
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Of een gebruiker als actief moet worden beschouwd. Deselecteer dit in plaats van het account te deleten."
|
||||
|
||||
#: build/lib/core/models.py:202 core/models.py:202
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
msgid "user"
|
||||
msgstr "gebruiker"
|
||||
|
||||
#: build/lib/core/models.py:203 core/models.py:203
|
||||
#: build/lib/core/models.py:205 core/models.py:205
|
||||
msgid "users"
|
||||
msgstr "gebruikers"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
|
||||
#: core/models.py:361 core/models.py:1284
|
||||
msgid "title"
|
||||
msgstr "titel"
|
||||
|
||||
#: build/lib/core/models.py:360 core/models.py:360
|
||||
#: build/lib/core/models.py:362 core/models.py:362
|
||||
msgid "excerpt"
|
||||
msgstr "uittreksel"
|
||||
|
||||
#: build/lib/core/models.py:409 core/models.py:409
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Document"
|
||||
msgstr "Document"
|
||||
|
||||
#: build/lib/core/models.py:410 core/models.py:410
|
||||
#: build/lib/core/models.py:412 core/models.py:412
|
||||
msgid "Documents"
|
||||
msgstr "Documenten"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||
#: core/models.py:822
|
||||
msgid "Untitled Document"
|
||||
msgstr "Naamloos Document"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:857 core/models.py:857
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} heeft een document met gedeeld!"
|
||||
msgstr "{name} heeft een document met u gedeeld!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:861 core/models.py:861
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} heeft u uitgenodigd met de rol \"{role}\" op het volgende document:"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:867 core/models.py:867
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} heeft een document met u gedeeld: {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Document/gebruiker url"
|
||||
msgstr "Document/gebruiker link"
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:968 core/models.py:968
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Document/gebruiker url"
|
||||
msgstr "Document/gebruiker link"
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Een url bestaat al voor dit document/deze gebruiker."
|
||||
msgstr "Een link bestaat al voor dit document/deze gebruiker."
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorite"
|
||||
msgstr "Document favoriet"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:998 core/models.py:998
|
||||
msgid "Document favorites"
|
||||
msgstr "Document favorieten"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Dit document is al in gebruik als favoriete door dezelfde gebruiker."
|
||||
msgstr "Dit document is al in gebruik als favoriet door dezelfde gebruiker."
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relation"
|
||||
msgstr "Document/gebruiker relatie"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||
msgid "Document/user relations"
|
||||
msgstr "Document/gebruiker relaties"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "This user is already in this document."
|
||||
msgstr "De gebruiker is al in dit document."
|
||||
msgstr "De gebruiker bestaat al in dit document."
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Het team is al in dit document."
|
||||
msgstr "Dit team bestaat al in dit document."
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
|
||||
#: core/models.py:1045 core/models.py:1370
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Een gebruiker of team moet gekozen worden, maar niet beide."
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for access"
|
||||
msgstr "Document verzoekt om toegang"
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Document verzoekt om toegangen"
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
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:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1263 core/models.py:1263
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} wenst toegang tot een document!"
|
||||
msgstr "{name} verzoekt toegang tot een document!"
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1267 core/models.py:1267
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} wenst toegang tot het volgende document:"
|
||||
msgstr "{name} verzoekt toegang tot het volgende document:"
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1273 core/models.py:1273
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name} vraagt toegang tot het document: {title}"
|
||||
msgstr "{name} verzoekt toegang tot het document: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "description"
|
||||
msgstr "omschrijving"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "public"
|
||||
msgstr "publiek"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: build/lib/core/models.py:1291 core/models.py:1291
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Of dit template als publiek is en door iedereen te gebruiken is."
|
||||
msgstr "Of dit sjabloon door iedereen publiekelijk te gebruiken is."
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Template"
|
||||
msgstr "Template"
|
||||
msgstr "Sjabloon"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
msgid "Templates"
|
||||
msgstr "Templates"
|
||||
msgstr "Sjabloon"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relation"
|
||||
msgstr "Template/gebruiker relatie"
|
||||
msgstr "Sjabloon/gebruiker relatie"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1352 core/models.py:1352
|
||||
msgid "Template/user relations"
|
||||
msgstr "Template/gebruiker relaties"
|
||||
msgstr "Sjabloon/gebruiker relaties"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1358 core/models.py:1358
|
||||
msgid "This user is already in this template."
|
||||
msgstr "De gebruiker bestaat al in dit template."
|
||||
msgstr "De gebruiker bestaat al in dit sjabloon."
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1364 core/models.py:1364
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Het team bestaat al in dit template."
|
||||
msgstr "Het team bestaat al in dit sjabloon."
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "email address"
|
||||
msgstr "email adres"
|
||||
msgstr "e-mailadres"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitation"
|
||||
msgstr "Document uitnodiging"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1461 core/models.py:1461
|
||||
msgid "Document invitations"
|
||||
msgstr "Document uitnodigingen"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1481 core/models.py:1481
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
|
||||
"PO-Revision-Date: 2025-11-10 09:54\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Portuguese\n"
|
||||
"Language: pt_PT\n"
|
||||
@@ -17,20 +17,20 @@ msgstr ""
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 18\n"
|
||||
|
||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
||||
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||
msgid "Personal info"
|
||||
msgstr "Informações Pessoais"
|
||||
|
||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
||||
#: core/admin.py:138
|
||||
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||
#: core/admin.py:137
|
||||
msgid "Permissions"
|
||||
msgstr "Permissões"
|
||||
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||
msgid "Important dates"
|
||||
msgstr "Datas importantes"
|
||||
|
||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
||||
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||
msgid "Tree structure"
|
||||
msgstr "Estrutura de árvore"
|
||||
|
||||
@@ -79,7 +79,7 @@ msgstr "Tipo de corpo"
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "cópia de {title}"
|
||||
@@ -178,228 +178,228 @@ msgstr ""
|
||||
msgid "full name"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:151 core/models.py:151
|
||||
#: build/lib/core/models.py:152 core/models.py:152
|
||||
msgid "short name"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:153 core/models.py:153
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:158 core/models.py:158
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:165 core/models.py:165
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "language"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:166 core/models.py:166
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
#: build/lib/core/models.py:176 core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:177 core/models.py:177
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
msgid "device"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
#: build/lib/core/models.py:181 core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:182 core/models.py:182
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:187 core/models.py:187
|
||||
#: build/lib/core/models.py:189 core/models.py:189
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:190 core/models.py:190
|
||||
#: build/lib/core/models.py:192 core/models.py:192
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:202 core/models.py:202
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:203 core/models.py:203
|
||||
#: build/lib/core/models.py:205 core/models.py:205
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
|
||||
#: core/models.py:361 core/models.py:1284
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:360 core/models.py:360
|
||||
#: build/lib/core/models.py:362 core/models.py:362
|
||||
msgid "excerpt"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:409 core/models.py:409
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:410 core/models.py:410
|
||||
#: build/lib/core/models.py:412 core/models.py:412
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||
#: core/models.py:822
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:857 core/models.py:857
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:861 core/models.py:861
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:867 core/models.py:867
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:968 core/models.py:968
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:998 core/models.py:998
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
|
||||
#: core/models.py:1045 core/models.py:1370
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1263 core/models.py:1263
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1267 core/models.py:1267
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1273 core/models.py:1273
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: build/lib/core/models.py:1291 core/models.py:1291
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1352 core/models.py:1352
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1358 core/models.py:1358
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1364 core/models.py:1364
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1461 core/models.py:1461
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1481 core/models.py:1481
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
|
||||
"PO-Revision-Date: 2025-11-10 09:54\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Russian\n"
|
||||
"Language: ru_RU\n"
|
||||
@@ -17,20 +17,20 @@ msgstr ""
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 18\n"
|
||||
|
||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
||||
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||
msgid "Personal info"
|
||||
msgstr "Личная информация"
|
||||
|
||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
||||
#: core/admin.py:138
|
||||
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||
#: core/admin.py:137
|
||||
msgid "Permissions"
|
||||
msgstr "Разрешения"
|
||||
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||
msgid "Important dates"
|
||||
msgstr "Важные даты"
|
||||
|
||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
||||
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||
msgid "Tree structure"
|
||||
msgstr "Древовидная структура"
|
||||
|
||||
@@ -79,7 +79,7 @@ msgstr "Тип сообщения"
|
||||
msgid "Format"
|
||||
msgstr "Формат"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "копия {title}"
|
||||
@@ -178,228 +178,228 @@ msgstr "Обязательно. 255 символов или меньше. Тол
|
||||
msgid "full name"
|
||||
msgstr "полное имя"
|
||||
|
||||
#: build/lib/core/models.py:151 core/models.py:151
|
||||
#: build/lib/core/models.py:152 core/models.py:152
|
||||
msgid "short name"
|
||||
msgstr "короткое имя"
|
||||
|
||||
#: build/lib/core/models.py:153 core/models.py:153
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr "личный адрес электронной почты"
|
||||
|
||||
#: build/lib/core/models.py:158 core/models.py:158
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr "e-mail администратора"
|
||||
|
||||
#: build/lib/core/models.py:165 core/models.py:165
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "language"
|
||||
msgstr "язык"
|
||||
|
||||
#: build/lib/core/models.py:166 core/models.py:166
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "Язык, на котором пользователь хочет видеть интерфейс."
|
||||
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
#: build/lib/core/models.py:176 core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Часовой пояс, в котором пользователь хочет видеть время."
|
||||
|
||||
#: build/lib/core/models.py:177 core/models.py:177
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "устройство"
|
||||
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
#: build/lib/core/models.py:181 core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Пользователь является устройством или человеком."
|
||||
|
||||
#: build/lib/core/models.py:182 core/models.py:182
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "статус сотрудника"
|
||||
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Может ли пользователь войти на этот административный сайт."
|
||||
|
||||
#: build/lib/core/models.py:187 core/models.py:187
|
||||
#: build/lib/core/models.py:189 core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "активный"
|
||||
|
||||
#: build/lib/core/models.py:190 core/models.py:190
|
||||
#: build/lib/core/models.py:192 core/models.py:192
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Должен ли пользователь рассматриваться как активный. Альтернатива удалению учётных записей."
|
||||
|
||||
#: build/lib/core/models.py:202 core/models.py:202
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
msgid "user"
|
||||
msgstr "пользователь"
|
||||
|
||||
#: build/lib/core/models.py:203 core/models.py:203
|
||||
#: build/lib/core/models.py:205 core/models.py:205
|
||||
msgid "users"
|
||||
msgstr "пользователи"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
|
||||
#: core/models.py:361 core/models.py:1284
|
||||
msgid "title"
|
||||
msgstr "заголовок"
|
||||
|
||||
#: build/lib/core/models.py:360 core/models.py:360
|
||||
#: build/lib/core/models.py:362 core/models.py:362
|
||||
msgid "excerpt"
|
||||
msgstr "отрывок"
|
||||
|
||||
#: build/lib/core/models.py:409 core/models.py:409
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Document"
|
||||
msgstr "Документ"
|
||||
|
||||
#: build/lib/core/models.py:410 core/models.py:410
|
||||
#: build/lib/core/models.py:412 core/models.py:412
|
||||
msgid "Documents"
|
||||
msgstr "Документы"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||
#: core/models.py:822
|
||||
msgid "Untitled Document"
|
||||
msgstr "Безымянный документ"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:857 core/models.py:857
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} делится с вами документом!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:861 core/models.py:861
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:867 core/models.py:867
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} делится с вами документом: {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Трассировка связи документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:968 core/models.py:968
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Трассировка связей документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorite"
|
||||
msgstr "Избранный документ"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:998 core/models.py:998
|
||||
msgid "Document favorites"
|
||||
msgstr "Избранные документы"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Этот документ уже помечен как избранный для этого пользователя."
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relation"
|
||||
msgstr "Отношение документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||
msgid "Document/user relations"
|
||||
msgstr "Отношения документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Этот пользователь уже имеет доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Эта команда уже имеет доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
|
||||
#: core/models.py:1045 core/models.py:1370
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for access"
|
||||
msgstr "Документ запрашивает доступ"
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Документ запрашивает доступы"
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "Этот пользователь уже запросил доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1263 core/models.py:1263
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} хочет получить доступ к документу!"
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1267 core/models.py:1267
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} хочет получить доступ к следующему документу:"
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1273 core/models.py:1273
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name} запрашивает доступ к документу: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "description"
|
||||
msgstr "описание"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "code"
|
||||
msgstr "код"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "public"
|
||||
msgstr "доступно всем"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: build/lib/core/models.py:1291 core/models.py:1291
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Этот шаблон доступен всем пользователям."
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Template"
|
||||
msgstr "Шаблон"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
msgid "Templates"
|
||||
msgstr "Шаблоны"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relation"
|
||||
msgstr "Отношение шаблон/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1352 core/models.py:1352
|
||||
msgid "Template/user relations"
|
||||
msgstr "Отношения шаблон/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1358 core/models.py:1358
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Этот пользователь уже указан в этом шаблоне."
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1364 core/models.py:1364
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Эта команда уже указана в этом шаблоне."
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "email address"
|
||||
msgstr "адрес электронной почты"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitation"
|
||||
msgstr "Приглашение для документа"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1461 core/models.py:1461
|
||||
msgid "Document invitations"
|
||||
msgstr "Приглашения для документов"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1481 core/models.py:1481
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Этот адрес уже связан с зарегистрированным пользователем."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
|
||||
"PO-Revision-Date: 2025-11-10 09:54\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Slovenian\n"
|
||||
"Language: sl_SI\n"
|
||||
@@ -17,20 +17,20 @@ msgstr ""
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 18\n"
|
||||
|
||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
||||
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||
msgid "Personal info"
|
||||
msgstr "Osebni podatki"
|
||||
|
||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
||||
#: core/admin.py:138
|
||||
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||
#: core/admin.py:137
|
||||
msgid "Permissions"
|
||||
msgstr "Dovoljenja"
|
||||
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||
msgid "Important dates"
|
||||
msgstr "Pomembni datumi"
|
||||
|
||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
||||
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||
msgid "Tree structure"
|
||||
msgstr "Drevesna struktura"
|
||||
|
||||
@@ -79,7 +79,7 @@ msgstr "Vrsta telesa"
|
||||
msgid "Format"
|
||||
msgstr "Oblika"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
@@ -178,228 +178,228 @@ msgstr ""
|
||||
msgid "full name"
|
||||
msgstr "polno ime"
|
||||
|
||||
#: build/lib/core/models.py:151 core/models.py:151
|
||||
#: build/lib/core/models.py:152 core/models.py:152
|
||||
msgid "short name"
|
||||
msgstr "kratko ime"
|
||||
|
||||
#: build/lib/core/models.py:153 core/models.py:153
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr "elektronski naslov identitete"
|
||||
|
||||
#: build/lib/core/models.py:158 core/models.py:158
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr "elektronski naslov skrbnika"
|
||||
|
||||
#: build/lib/core/models.py:165 core/models.py:165
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "language"
|
||||
msgstr "jezik"
|
||||
|
||||
#: build/lib/core/models.py:166 core/models.py:166
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "Jezik, v katerem uporabnik želi videti vmesnik."
|
||||
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
#: build/lib/core/models.py:176 core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Časovni pas, v katerem želi uporabnik videti uro."
|
||||
|
||||
#: build/lib/core/models.py:177 core/models.py:177
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "naprava"
|
||||
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
#: build/lib/core/models.py:181 core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Ali je uporabnik naprava ali pravi uporabnik."
|
||||
|
||||
#: build/lib/core/models.py:182 core/models.py:182
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "kadrovski status"
|
||||
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Ali se uporabnik lahko prijavi na to skrbniško mesto."
|
||||
|
||||
#: build/lib/core/models.py:187 core/models.py:187
|
||||
#: build/lib/core/models.py:189 core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "aktivni"
|
||||
|
||||
#: build/lib/core/models.py:190 core/models.py:190
|
||||
#: build/lib/core/models.py:192 core/models.py:192
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Ali je treba tega uporabnika obravnavati kot aktivnega. Namesto brisanja računov počistite to izbiro."
|
||||
|
||||
#: build/lib/core/models.py:202 core/models.py:202
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
msgid "user"
|
||||
msgstr "uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:203 core/models.py:203
|
||||
#: build/lib/core/models.py:205 core/models.py:205
|
||||
msgid "users"
|
||||
msgstr "uporabniki"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
|
||||
#: core/models.py:361 core/models.py:1284
|
||||
msgid "title"
|
||||
msgstr "naslov"
|
||||
|
||||
#: build/lib/core/models.py:360 core/models.py:360
|
||||
#: build/lib/core/models.py:362 core/models.py:362
|
||||
msgid "excerpt"
|
||||
msgstr "odlomek"
|
||||
|
||||
#: build/lib/core/models.py:409 core/models.py:409
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Document"
|
||||
msgstr "Dokument"
|
||||
|
||||
#: build/lib/core/models.py:410 core/models.py:410
|
||||
#: build/lib/core/models.py:412 core/models.py:412
|
||||
msgid "Documents"
|
||||
msgstr "Dokumenti"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||
#: core/models.py:822
|
||||
msgid "Untitled Document"
|
||||
msgstr "Dokument brez naslova"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:857 core/models.py:857
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} je delil dokument z vami!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:861 core/models.py:861
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} vas je povabil z vlogo \"{role}\" na naslednjem dokumentu:"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:867 core/models.py:867
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} je delil dokument z vami: {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Dokument/sled povezave uporabnika"
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:968 core/models.py:968
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Sledi povezav dokumenta/uporabnika"
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorite"
|
||||
msgstr "Priljubljeni dokument"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:998 core/models.py:998
|
||||
msgid "Document favorites"
|
||||
msgstr "Priljubljeni dokumenti"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Ta dokument je že ciljno usmerjen s priljubljenim primerkom relacije za istega uporabnika."
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relation"
|
||||
msgstr "Odnos dokument/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||
msgid "Document/user relations"
|
||||
msgstr "Odnosi dokument/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Ta uporabnik je že v tem dokumentu."
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ta ekipa je že v tem dokumentu."
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
|
||||
#: core/models.py:1045 core/models.py:1370
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Nastaviti je treba bodisi uporabnika ali ekipo, a ne obojega."
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1263 core/models.py:1263
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1267 core/models.py:1267
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1273 core/models.py:1273
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "description"
|
||||
msgstr "opis"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "code"
|
||||
msgstr "koda"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "public"
|
||||
msgstr "javno"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: build/lib/core/models.py:1291 core/models.py:1291
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Ali je ta predloga javna za uporabo."
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Template"
|
||||
msgstr "Predloga"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
msgid "Templates"
|
||||
msgstr "Predloge"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relation"
|
||||
msgstr "Odnos predloga/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1352 core/models.py:1352
|
||||
msgid "Template/user relations"
|
||||
msgstr "Odnosi med predlogo in uporabnikom"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1358 core/models.py:1358
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Ta uporabnik je že v tej predlogi."
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1364 core/models.py:1364
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Ta ekipa je že v tej predlogi."
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "email address"
|
||||
msgstr "elektronski naslov"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitation"
|
||||
msgstr "Vabilo na dokument"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1461 core/models.py:1461
|
||||
msgid "Document invitations"
|
||||
msgstr "Vabila na dokument"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1481 core/models.py:1481
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
|
||||
"PO-Revision-Date: 2025-11-10 09:54\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Swedish\n"
|
||||
"Language: sv_SE\n"
|
||||
@@ -17,20 +17,20 @@ msgstr ""
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 18\n"
|
||||
|
||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
||||
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||
msgid "Personal info"
|
||||
msgstr "Personuppgifter"
|
||||
|
||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
||||
#: core/admin.py:138
|
||||
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||
#: core/admin.py:137
|
||||
msgid "Permissions"
|
||||
msgstr "Behörigheter"
|
||||
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||
msgid "Important dates"
|
||||
msgstr "Viktiga datum"
|
||||
|
||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
||||
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||
msgid "Tree structure"
|
||||
msgstr ""
|
||||
|
||||
@@ -79,7 +79,7 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
@@ -178,228 +178,228 @@ msgstr ""
|
||||
msgid "full name"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:151 core/models.py:151
|
||||
#: build/lib/core/models.py:152 core/models.py:152
|
||||
msgid "short name"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:153 core/models.py:153
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:158 core/models.py:158
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:165 core/models.py:165
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "language"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:166 core/models.py:166
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
#: build/lib/core/models.py:176 core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:177 core/models.py:177
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
msgid "device"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
#: build/lib/core/models.py:181 core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:182 core/models.py:182
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:187 core/models.py:187
|
||||
#: build/lib/core/models.py:189 core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "aktiv"
|
||||
|
||||
#: build/lib/core/models.py:190 core/models.py:190
|
||||
#: build/lib/core/models.py:192 core/models.py:192
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:202 core/models.py:202
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:203 core/models.py:203
|
||||
#: build/lib/core/models.py:205 core/models.py:205
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
|
||||
#: core/models.py:361 core/models.py:1284
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:360 core/models.py:360
|
||||
#: build/lib/core/models.py:362 core/models.py:362
|
||||
msgid "excerpt"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:409 core/models.py:409
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:410 core/models.py:410
|
||||
#: build/lib/core/models.py:412 core/models.py:412
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||
#: core/models.py:822
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:857 core/models.py:857
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:861 core/models.py:861
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:867 core/models.py:867
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:968 core/models.py:968
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:998 core/models.py:998
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
|
||||
#: core/models.py:1045 core/models.py:1370
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1263 core/models.py:1263
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1267 core/models.py:1267
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1273 core/models.py:1273
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: build/lib/core/models.py:1291 core/models.py:1291
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1352 core/models.py:1352
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1358 core/models.py:1358
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1364 core/models.py:1364
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "email address"
|
||||
msgstr "e-postadress"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitation"
|
||||
msgstr "Bjud in dokument"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1461 core/models.py:1461
|
||||
msgid "Document invitations"
|
||||
msgstr "Inbjudningar dokument"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1481 core/models.py:1481
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Denna e-postadress är redan associerad med en registrerad användare."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
|
||||
"PO-Revision-Date: 2025-11-10 09:54\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Turkish\n"
|
||||
"Language: tr_TR\n"
|
||||
@@ -17,20 +17,20 @@ msgstr ""
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 18\n"
|
||||
|
||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
||||
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||
msgid "Personal info"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
||||
#: core/admin.py:138
|
||||
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||
#: core/admin.py:137
|
||||
msgid "Permissions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||
msgid "Important dates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
||||
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||
msgid "Tree structure"
|
||||
msgstr ""
|
||||
|
||||
@@ -79,7 +79,7 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
@@ -178,228 +178,228 @@ msgstr ""
|
||||
msgid "full name"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:151 core/models.py:151
|
||||
#: build/lib/core/models.py:152 core/models.py:152
|
||||
msgid "short name"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:153 core/models.py:153
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:158 core/models.py:158
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:165 core/models.py:165
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "language"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:166 core/models.py:166
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
#: build/lib/core/models.py:176 core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:177 core/models.py:177
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
msgid "device"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
#: build/lib/core/models.py:181 core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:182 core/models.py:182
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:187 core/models.py:187
|
||||
#: build/lib/core/models.py:189 core/models.py:189
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:190 core/models.py:190
|
||||
#: build/lib/core/models.py:192 core/models.py:192
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:202 core/models.py:202
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:203 core/models.py:203
|
||||
#: build/lib/core/models.py:205 core/models.py:205
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
|
||||
#: core/models.py:361 core/models.py:1284
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:360 core/models.py:360
|
||||
#: build/lib/core/models.py:362 core/models.py:362
|
||||
msgid "excerpt"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:409 core/models.py:409
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:410 core/models.py:410
|
||||
#: build/lib/core/models.py:412 core/models.py:412
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||
#: core/models.py:822
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:857 core/models.py:857
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:861 core/models.py:861
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:867 core/models.py:867
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:968 core/models.py:968
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:998 core/models.py:998
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
|
||||
#: core/models.py:1045 core/models.py:1370
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1263 core/models.py:1263
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1267 core/models.py:1267
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1273 core/models.py:1273
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: build/lib/core/models.py:1291 core/models.py:1291
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1352 core/models.py:1352
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1358 core/models.py:1358
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1364 core/models.py:1364
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1461 core/models.py:1461
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1481 core/models.py:1481
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
|
||||
"PO-Revision-Date: 2025-11-10 09:54\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Ukrainian\n"
|
||||
"Language: uk_UA\n"
|
||||
@@ -17,20 +17,20 @@ msgstr ""
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 18\n"
|
||||
|
||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
||||
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||
msgid "Personal info"
|
||||
msgstr "Особисті дані"
|
||||
|
||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
||||
#: core/admin.py:138
|
||||
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||
#: core/admin.py:137
|
||||
msgid "Permissions"
|
||||
msgstr "Дозволи"
|
||||
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||
msgid "Important dates"
|
||||
msgstr "Важливі дати"
|
||||
|
||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
||||
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||
msgid "Tree structure"
|
||||
msgstr "Ієрархічна структура"
|
||||
|
||||
@@ -79,7 +79,7 @@ msgstr "Тип вмісту"
|
||||
msgid "Format"
|
||||
msgstr "Формат"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "копія {title}"
|
||||
@@ -178,228 +178,228 @@ msgstr "Обов'язкове. 255 символів або менше. Тіль
|
||||
msgid "full name"
|
||||
msgstr "повне ім'я"
|
||||
|
||||
#: build/lib/core/models.py:151 core/models.py:151
|
||||
#: build/lib/core/models.py:152 core/models.py:152
|
||||
msgid "short name"
|
||||
msgstr "коротке ім'я"
|
||||
|
||||
#: build/lib/core/models.py:153 core/models.py:153
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr "адреса електронної пошти особи"
|
||||
|
||||
#: build/lib/core/models.py:158 core/models.py:158
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr "електронна адреса адміністратора"
|
||||
|
||||
#: build/lib/core/models.py:165 core/models.py:165
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "language"
|
||||
msgstr "мова"
|
||||
|
||||
#: build/lib/core/models.py:166 core/models.py:166
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "Мова, якою користувач хоче бачити інтерфейс."
|
||||
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
#: build/lib/core/models.py:176 core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Часовий пояс, в якому користувач хоче бачити час."
|
||||
|
||||
#: build/lib/core/models.py:177 core/models.py:177
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "пристрій"
|
||||
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
#: build/lib/core/models.py:181 core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Чи є користувач пристроєм чи реальним користувачем."
|
||||
|
||||
#: build/lib/core/models.py:182 core/models.py:182
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "статус співробітника"
|
||||
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Чи може користувач увійти на цей сайт адміністратора."
|
||||
|
||||
#: build/lib/core/models.py:187 core/models.py:187
|
||||
#: build/lib/core/models.py:189 core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "активний"
|
||||
|
||||
#: build/lib/core/models.py:190 core/models.py:190
|
||||
#: build/lib/core/models.py:192 core/models.py:192
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Чи слід ставитися до цього користувача як до активного. Зніміть вибір замість видалення облікового запису."
|
||||
|
||||
#: build/lib/core/models.py:202 core/models.py:202
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
msgid "user"
|
||||
msgstr "користувач"
|
||||
|
||||
#: build/lib/core/models.py:203 core/models.py:203
|
||||
#: build/lib/core/models.py:205 core/models.py:205
|
||||
msgid "users"
|
||||
msgstr "користувачі"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
|
||||
#: core/models.py:361 core/models.py:1284
|
||||
msgid "title"
|
||||
msgstr "заголовок"
|
||||
|
||||
#: build/lib/core/models.py:360 core/models.py:360
|
||||
#: build/lib/core/models.py:362 core/models.py:362
|
||||
msgid "excerpt"
|
||||
msgstr "уривок"
|
||||
|
||||
#: build/lib/core/models.py:409 core/models.py:409
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Document"
|
||||
msgstr "Документ"
|
||||
|
||||
#: build/lib/core/models.py:410 core/models.py:410
|
||||
#: build/lib/core/models.py:412 core/models.py:412
|
||||
msgid "Documents"
|
||||
msgstr "Документи"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||
#: core/models.py:822
|
||||
msgid "Untitled Document"
|
||||
msgstr "Документ без назви"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:857 core/models.py:857
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} ділиться з вами документом!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:861 core/models.py:861
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:867 core/models.py:867
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} ділиться з вами документом: {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Трасування посилання Документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:968 core/models.py:968
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Трасування посилань Документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorite"
|
||||
msgstr "Обраний документ"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:998 core/models.py:998
|
||||
msgid "Document favorites"
|
||||
msgstr "Обрані документи"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Цей документ вже вказаний як обраний для одного користувача."
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relation"
|
||||
msgstr "Відносини документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||
msgid "Document/user relations"
|
||||
msgstr "Відносини документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Цей користувач вже має доступ до цього документу."
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ця команда вже має доступ до цього документа."
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
|
||||
#: core/models.py:1045 core/models.py:1370
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Вкажіть користувача або команду, а не обох."
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for access"
|
||||
msgstr "Запит доступу до документа"
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Запит доступу для документа"
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "Цей користувач вже попросив доступ до цього документа."
|
||||
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1263 core/models.py:1263
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} хоче отримати доступ до документа!"
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1267 core/models.py:1267
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} бажає отримати доступ до наступного документа:"
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1273 core/models.py:1273
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name} запитує доступ до документа: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "description"
|
||||
msgstr "опис"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "code"
|
||||
msgstr "код"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "public"
|
||||
msgstr "публічне"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: build/lib/core/models.py:1291 core/models.py:1291
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Чи є цей шаблон публічним для будь-кого користувача."
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Template"
|
||||
msgstr "Шаблон"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
msgid "Templates"
|
||||
msgstr "Шаблони"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relation"
|
||||
msgstr "Відношення шаблон/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1352 core/models.py:1352
|
||||
msgid "Template/user relations"
|
||||
msgstr "Відношення шаблон/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1358 core/models.py:1358
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Цей користувач вже має доступ до цього шаблону."
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1364 core/models.py:1364
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Ця команда вже має доступ до цього шаблону."
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "email address"
|
||||
msgstr "електронна адреса"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitation"
|
||||
msgstr "Запрошення до редагування документа"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1461 core/models.py:1461
|
||||
msgid "Document invitations"
|
||||
msgstr "Запрошення до редагування документів"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1481 core/models.py:1481
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
|
||||
"PO-Revision-Date: 2025-11-10 09:54\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"Language: zh_CN\n"
|
||||
@@ -17,20 +17,20 @@ msgstr ""
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 18\n"
|
||||
|
||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
||||
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||
msgid "Personal info"
|
||||
msgstr "个人信息"
|
||||
|
||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
||||
#: core/admin.py:138
|
||||
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||
#: core/admin.py:137
|
||||
msgid "Permissions"
|
||||
msgstr "权限"
|
||||
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||
msgid "Important dates"
|
||||
msgstr "重要日期"
|
||||
|
||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
||||
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||
msgid "Tree structure"
|
||||
msgstr "树状结构"
|
||||
|
||||
@@ -79,7 +79,7 @@ msgstr "正文类型"
|
||||
msgid "Format"
|
||||
msgstr "格式"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "{title} 的副本"
|
||||
@@ -178,228 +178,228 @@ msgstr "必填项。限255个字符以内。仅支持ASCII字符。"
|
||||
msgid "full name"
|
||||
msgstr "全名"
|
||||
|
||||
#: build/lib/core/models.py:151 core/models.py:151
|
||||
#: build/lib/core/models.py:152 core/models.py:152
|
||||
msgid "short name"
|
||||
msgstr "简称"
|
||||
|
||||
#: build/lib/core/models.py:153 core/models.py:153
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr "身份电子邮件地址"
|
||||
|
||||
#: build/lib/core/models.py:158 core/models.py:158
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr "管理员电子邮件地址"
|
||||
|
||||
#: build/lib/core/models.py:165 core/models.py:165
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "language"
|
||||
msgstr "语言"
|
||||
|
||||
#: build/lib/core/models.py:166 core/models.py:166
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "用户希望看到的界面语言。"
|
||||
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
#: build/lib/core/models.py:176 core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "用户查看时间希望的时区。"
|
||||
|
||||
#: build/lib/core/models.py:177 core/models.py:177
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "设备"
|
||||
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
#: build/lib/core/models.py:181 core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "用户是设备还是真实用户。"
|
||||
|
||||
#: build/lib/core/models.py:182 core/models.py:182
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "员工状态"
|
||||
|
||||
#: build/lib/core/models.py:184 core/models.py:184
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "用户是否可以登录该管理员站点。"
|
||||
|
||||
#: build/lib/core/models.py:187 core/models.py:187
|
||||
#: build/lib/core/models.py:189 core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "激活"
|
||||
|
||||
#: build/lib/core/models.py:190 core/models.py:190
|
||||
#: build/lib/core/models.py:192 core/models.py:192
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "是否应将此用户视为活跃用户。取消选择此选项而不是删除账户。"
|
||||
|
||||
#: build/lib/core/models.py:202 core/models.py:202
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
msgid "user"
|
||||
msgstr "用户"
|
||||
|
||||
#: build/lib/core/models.py:203 core/models.py:203
|
||||
#: build/lib/core/models.py:205 core/models.py:205
|
||||
msgid "users"
|
||||
msgstr "个用户"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
|
||||
#: core/models.py:361 core/models.py:1284
|
||||
msgid "title"
|
||||
msgstr "标题"
|
||||
|
||||
#: build/lib/core/models.py:360 core/models.py:360
|
||||
#: build/lib/core/models.py:362 core/models.py:362
|
||||
msgid "excerpt"
|
||||
msgstr "摘要"
|
||||
|
||||
#: build/lib/core/models.py:409 core/models.py:409
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Document"
|
||||
msgstr "文档"
|
||||
|
||||
#: build/lib/core/models.py:410 core/models.py:410
|
||||
#: build/lib/core/models.py:412 core/models.py:412
|
||||
msgid "Documents"
|
||||
msgstr "个文档"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||
#: core/models.py:822
|
||||
msgid "Untitled Document"
|
||||
msgstr "未命名文档"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:857 core/models.py:857
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} 与您共享了一个文档!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:861 core/models.py:861
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:867 core/models.py:867
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} 与您共享了一个文档:{title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link trace"
|
||||
msgstr "文档/用户链接跟踪"
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:968 core/models.py:968
|
||||
msgid "Document/user link traces"
|
||||
msgstr "个文档/用户链接跟踪"
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "此文档/用户的链接跟踪已存在。"
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorite"
|
||||
msgstr "文档收藏"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:998 core/models.py:998
|
||||
msgid "Document favorites"
|
||||
msgstr "文档收藏夹"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "该文档已被同一用户的收藏关系实例关联。"
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relation"
|
||||
msgstr "文档/用户关系"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||
msgid "Document/user relations"
|
||||
msgstr "文档/用户关系集"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "This user is already in this document."
|
||||
msgstr "该用户已在此文档中。"
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This team is already in this document."
|
||||
msgstr "该团队已在此文档中。"
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
|
||||
#: core/models.py:1045 core/models.py:1370
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "必须设置用户或团队之一,不能同时设置两者。"
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for access"
|
||||
msgstr "文档需要访问权限"
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "文档需要访问权限"
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "用户已申请该文档的访问权限。"
|
||||
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1263 core/models.py:1263
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} 申请访问文档!"
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1267 core/models.py:1267
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} 申请访问以下文档:"
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1273 core/models.py:1273
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name}申请文档:{title}的访问权限"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "description"
|
||||
msgstr "说明"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "code"
|
||||
msgstr "代码"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "public"
|
||||
msgstr "公开"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: build/lib/core/models.py:1291 core/models.py:1291
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "该模板是否公开供任何人使用。"
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Template"
|
||||
msgstr "模板"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
msgid "Templates"
|
||||
msgstr "模板"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relation"
|
||||
msgstr "模板/用户关系"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1352 core/models.py:1352
|
||||
msgid "Template/user relations"
|
||||
msgstr "模板/用户关系集"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1358 core/models.py:1358
|
||||
msgid "This user is already in this template."
|
||||
msgstr "该用户已在此模板中。"
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1364 core/models.py:1364
|
||||
msgid "This team is already in this template."
|
||||
msgstr "该团队已在此模板中。"
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "email address"
|
||||
msgstr "电子邮件地址"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitation"
|
||||
msgstr "文档邀请"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1461 core/models.py:1461
|
||||
msgid "Document invitations"
|
||||
msgstr "文档邀请"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1481 core/models.py:1481
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "此电子邮件已经与现有注册用户关联。"
|
||||
|
||||
|
||||
+30
-30
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "3.8.2"
|
||||
version = "3.9.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -25,42 +25,42 @@ license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"beautifulsoup4==4.13.4",
|
||||
"boto3==1.39.4",
|
||||
"Brotli==1.1.0",
|
||||
"beautifulsoup4==4.14.2",
|
||||
"boto3==1.40.59",
|
||||
"Brotli==1.2.0",
|
||||
"celery[redis]==5.5.3",
|
||||
"django-configurations==2.5.1",
|
||||
"django-cors-headers==4.7.0",
|
||||
"django-cors-headers==4.9.0",
|
||||
"django-countries==7.6.1",
|
||||
"django-csp==4.0",
|
||||
"django-filter==25.1",
|
||||
"django-lasuite[all]==0.0.14",
|
||||
"django-filter==25.2",
|
||||
"django-lasuite[all]==0.0.16",
|
||||
"django-parler==2.3",
|
||||
"django-redis==6.0.0",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django==5.2.7",
|
||||
"django==5.2.8",
|
||||
"django-treebeard==4.7.1",
|
||||
"djangorestframework==3.16.0",
|
||||
"djangorestframework==3.16.1",
|
||||
"drf_spectacular==0.28.0",
|
||||
"dockerflow==2024.4.2",
|
||||
"easy_thumbnails==2.10",
|
||||
"easy_thumbnails==2.10.1",
|
||||
"factory_boy==3.3.3",
|
||||
"gunicorn==23.0.0",
|
||||
"jsonschema==4.24.0",
|
||||
"lxml==6.0.0",
|
||||
"markdown==3.8.2",
|
||||
"jsonschema==4.25.1",
|
||||
"lxml==6.0.2",
|
||||
"markdown==3.9",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
"nested-multipart-parser==1.5.0",
|
||||
"openai==1.95.0",
|
||||
"psycopg[binary]==3.2.9",
|
||||
"pycrdt==0.12.25",
|
||||
"nested-multipart-parser==1.6.0",
|
||||
"openai==2.6.1",
|
||||
"psycopg[binary]==3.2.12",
|
||||
"pycrdt==0.12.42",
|
||||
"PyJWT==2.10.1",
|
||||
"python-magic==0.4.27",
|
||||
"redis<6.0.0",
|
||||
"requests==2.32.4",
|
||||
"sentry-sdk==2.32.0",
|
||||
"whitenoise==6.9.0",
|
||||
"requests==2.32.5",
|
||||
"sentry-sdk==2.42.1",
|
||||
"whitenoise==6.11.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -73,21 +73,21 @@ dependencies = [
|
||||
dev = [
|
||||
"django-extensions==4.1",
|
||||
"django-test-migrations==1.5.0",
|
||||
"drf-spectacular-sidecar==2025.7.1",
|
||||
"freezegun==1.5.2",
|
||||
"drf-spectacular-sidecar==2025.10.1",
|
||||
"freezegun==1.5.5",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==9.4.0",
|
||||
"pyfakefs==5.9.1",
|
||||
"ipython==9.6.0",
|
||||
"pyfakefs==5.10.0",
|
||||
"pylint-django==2.6.1",
|
||||
"pylint==3.3.7",
|
||||
"pytest-cov==6.2.1",
|
||||
"pylint<4.0.0",
|
||||
"pytest-cov==7.0.0",
|
||||
"pytest-django==4.11.1",
|
||||
"pytest==8.4.1",
|
||||
"pytest==8.4.2",
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.8.0",
|
||||
"responses==0.25.7",
|
||||
"ruff==0.12.2",
|
||||
"types-requests==2.32.4.20250611",
|
||||
"responses==0.25.8",
|
||||
"ruff==0.14.2",
|
||||
"types-requests==2.32.4.20250913",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
|
||||
@@ -13,7 +13,11 @@ import {
|
||||
} from './utils-common';
|
||||
import { getEditor, openSuggestionMenu, writeInEditor } from './utils-editor';
|
||||
import { connectOtherUserToDoc, updateShareLink } from './utils-share';
|
||||
import { createRootSubPage, navigateToPageFromTree } from './utils-sub-pages';
|
||||
import {
|
||||
createRootSubPage,
|
||||
getTreeRow,
|
||||
navigateToPageFromTree,
|
||||
} from './utils-sub-pages';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -490,7 +494,7 @@ test.describe('Doc Editor', () => {
|
||||
if (request.method().includes('GET')) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
status: requestCount ? 'ready' : 'processing',
|
||||
status: requestCount > 1 ? 'ready' : 'processing',
|
||||
file: '/anything.html',
|
||||
},
|
||||
});
|
||||
@@ -514,6 +518,12 @@ test.describe('Doc Editor', () => {
|
||||
await fileChooser.setFiles(path.join(__dirname, 'assets/test.html'));
|
||||
|
||||
await expect(editor.getByText('Analyzing file...')).toBeVisible();
|
||||
|
||||
// To be sure the retry happens even after a page reload
|
||||
await page.reload();
|
||||
|
||||
await expect(editor.getByText('Analyzing file...')).toBeVisible();
|
||||
|
||||
// The retry takes a few seconds
|
||||
await expect(editor.getByText('test.html')).toBeVisible({
|
||||
timeout: 7000,
|
||||
@@ -676,10 +686,9 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
await calloutBlock.locator('.inline-content').fill('example text');
|
||||
|
||||
await expect(page.locator('.bn-block').first()).toHaveAttribute(
|
||||
'data-background-color',
|
||||
'yellow',
|
||||
);
|
||||
await expect(
|
||||
page.locator('.bn-block-content[data-content-type="callout"]').first(),
|
||||
).toHaveAttribute('data-background-color', 'yellow');
|
||||
|
||||
const emojiButton = calloutBlock.getByRole('button');
|
||||
await expect(emojiButton).toHaveText('💡');
|
||||
@@ -703,10 +712,9 @@ test.describe('Doc Editor', () => {
|
||||
await page.locator('.mantine-Menu-dropdown > button').last().click();
|
||||
await page.locator('.bn-color-picker-dropdown > button').last().click();
|
||||
|
||||
await expect(page.locator('.bn-block').first()).toHaveAttribute(
|
||||
'data-background-color',
|
||||
'pink',
|
||||
);
|
||||
await expect(
|
||||
page.locator('.bn-block-content[data-content-type="callout"]').first(),
|
||||
).toHaveAttribute('data-background-color', 'pink');
|
||||
});
|
||||
|
||||
test('it checks interlink feature', async ({ page, browserName }) => {
|
||||
@@ -730,7 +738,13 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
await verifyDocName(page, docChild2);
|
||||
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
const treeRow = await getTreeRow(page, docChild2);
|
||||
await treeRow.locator('.--docs--doc-icon').click();
|
||||
await page.getByRole('button', { name: '😀' }).first().click();
|
||||
|
||||
await navigateToPageFromTree({ page, title: docChild1 });
|
||||
|
||||
await openSuggestionMenu({ page });
|
||||
await page.getByText('Link a doc').first().click();
|
||||
|
||||
const input = page.locator(
|
||||
@@ -744,6 +758,16 @@ test.describe('Doc Editor', () => {
|
||||
await expect(searchContainer.getByText(docChild1)).toBeVisible();
|
||||
await expect(searchContainer.getByText(docChild2)).toBeVisible();
|
||||
|
||||
const searchContainerRow = searchContainer
|
||||
.getByRole('option')
|
||||
.filter({
|
||||
hasText: docChild2,
|
||||
})
|
||||
.first();
|
||||
|
||||
await expect(searchContainerRow).toContainText('😀');
|
||||
await expect(searchContainerRow.locator('svg').first()).toBeHidden();
|
||||
|
||||
await input.pressSequentially('-child');
|
||||
|
||||
await expect(searchContainer.getByText(docChild1)).toBeVisible();
|
||||
@@ -758,32 +782,30 @@ test.describe('Doc Editor', () => {
|
||||
await expect(searchContainer).toBeHidden();
|
||||
|
||||
// Wait for the interlink to be created and rendered
|
||||
const editor = page.locator('.ProseMirror.bn-editor');
|
||||
const editor = await getEditor({ page });
|
||||
|
||||
const interlink = editor.getByRole('button', {
|
||||
const interlinkChild2 = editor.getByRole('button', {
|
||||
name: docChild2,
|
||||
});
|
||||
|
||||
await expect(interlink).toBeVisible({ timeout: 10000 });
|
||||
await interlink.click();
|
||||
await expect(interlinkChild2).toBeVisible({ timeout: 10000 });
|
||||
await expect(interlinkChild2).toContainText('😀');
|
||||
await expect(interlinkChild2.locator('svg').first()).toBeHidden();
|
||||
await interlinkChild2.click();
|
||||
|
||||
await verifyDocName(page, docChild2);
|
||||
});
|
||||
|
||||
test('it checks interlink shortcut @', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-interlink', browserName, 1);
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
const editor = page.locator('.bn-block-outer').last();
|
||||
await editor.click();
|
||||
await page.keyboard.press('@');
|
||||
|
||||
await expect(
|
||||
page.locator(
|
||||
"span[data-inline-content-type='interlinkingSearchInline'] input",
|
||||
),
|
||||
).toBeVisible();
|
||||
await page.keyboard.press('@');
|
||||
await input.fill(docChild1);
|
||||
await searchContainer.getByText(docChild1).click();
|
||||
|
||||
const interlinkChild1 = editor.getByRole('button', {
|
||||
name: docChild1,
|
||||
});
|
||||
await expect(interlinkChild1).toBeVisible({ timeout: 10000 });
|
||||
await expect(interlinkChild1.locator('svg').first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('it checks multiple big doc scroll to the top', async ({
|
||||
@@ -844,10 +866,10 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
await expect(pdfBlock).toBeVisible();
|
||||
|
||||
await page.getByText('Add PDF').click();
|
||||
await page.getByText(/Add (PDF|file)/).click();
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByText('Upload file').click();
|
||||
await page.getByText(/Upload (PDF|file)/).click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
|
||||
await fileChooser.setFiles(path.join(__dirname, 'assets/test-pdf.pdf'));
|
||||
|
||||
@@ -2,12 +2,11 @@ import path from 'path';
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import cs from 'convert-stream';
|
||||
import pdf from 'pdf-parse';
|
||||
import { PDFParse } from 'pdf-parse';
|
||||
|
||||
import {
|
||||
TestLanguage,
|
||||
createDoc,
|
||||
randomName,
|
||||
verifyDocName,
|
||||
waitForLanguageSwitch,
|
||||
} from './utils-common';
|
||||
@@ -59,20 +58,16 @@ test.describe('Doc Export', () => {
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
const editor = page.locator('.ProseMirror.bn-editor');
|
||||
|
||||
await editor.click();
|
||||
await editor.locator('.bn-block-outer').last().fill('Hello');
|
||||
|
||||
const editor = await writeInEditor({ page, text: 'Hello' });
|
||||
await page.keyboard.press('Enter');
|
||||
await editor.locator('.bn-block-outer').last().fill('/');
|
||||
await openSuggestionMenu({ page });
|
||||
await page.getByText('Page Break').click();
|
||||
|
||||
await expect(editor.locator('.bn-page-break')).toBeVisible();
|
||||
await expect(
|
||||
editor.locator('div[data-content-type="pageBreak"]'),
|
||||
).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await editor.locator('.bn-block-outer').last().fill('World');
|
||||
await writeInEditor({ page, text: 'World' });
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
@@ -90,11 +85,16 @@ test.describe('Doc Export', () => {
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfData = await pdf(pdfBuffer);
|
||||
const pdfParse = new PDFParse({ data: pdfBuffer });
|
||||
const pdfInfo = await pdfParse.getInfo();
|
||||
const pdfText = await pdfParse.getText();
|
||||
|
||||
expect(pdfData.numpages).toBe(2);
|
||||
expect(pdfData.text).toContain('\n\nHello\n\nWorld'); // This is the doc text
|
||||
expect(pdfData.info.Title).toBe(randomDoc);
|
||||
expect(pdfInfo.total).toBe(2);
|
||||
expect(pdfText.pages).toStrictEqual([
|
||||
{ text: 'Hello', num: 1 },
|
||||
{ text: 'World', num: 2 },
|
||||
]);
|
||||
expect(pdfInfo?.info.Title).toBe(randomDoc);
|
||||
});
|
||||
|
||||
test('it exports the doc to docx', async ({ page, browserName }) => {
|
||||
@@ -223,10 +223,10 @@ test.describe('Doc Export', () => {
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfExport = await pdf(pdfBuffer);
|
||||
const pdfText = pdfExport.text;
|
||||
|
||||
expect(pdfText).toContain('Hello World');
|
||||
const pdfParse = new PDFParse({ data: pdfBuffer });
|
||||
const pdfText = await pdfParse.getText();
|
||||
expect(pdfText.text).toContain('Hello World');
|
||||
});
|
||||
|
||||
test('it exports the doc with quotes', async ({ page, browserName }) => {
|
||||
@@ -269,52 +269,9 @@ test.describe('Doc Export', () => {
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfData = await pdf(pdfBuffer);
|
||||
|
||||
expect(pdfData.text).toContain('Hello World'); // This is the pdf text
|
||||
});
|
||||
|
||||
/**
|
||||
* We cannot assert the line break is visible in the pdf, but we can assert the
|
||||
* line break is visible in the editor and that the pdf is generated.
|
||||
*/
|
||||
test('it exports the doc with divider', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'export-divider', browserName, 1);
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
await editor.click();
|
||||
await editor.fill('Hello World');
|
||||
|
||||
// Trigger slash menu to show menu
|
||||
await editor.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Add a horizontal line').click();
|
||||
|
||||
await expect(
|
||||
editor.locator('.bn-block-content[data-content-type="divider"]'),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByTestId('doc-open-modal-download-button'),
|
||||
).toBeVisible();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
});
|
||||
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfData = await pdf(pdfBuffer);
|
||||
expect(pdfData.text).toContain('Hello World');
|
||||
const pdfParse = new PDFParse({ data: pdfBuffer });
|
||||
const pdfText = await pdfParse.getText();
|
||||
expect(pdfText.text).toContain('Hello World');
|
||||
});
|
||||
|
||||
test('it exports the doc with multi columns', async ({
|
||||
@@ -367,46 +324,33 @@ test.describe('Doc Export', () => {
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfData = await pdf(pdfBuffer);
|
||||
expect(pdfData.text).toContain('Column 1');
|
||||
expect(pdfData.text).toContain('Column 2');
|
||||
expect(pdfData.text).toContain('Column 3');
|
||||
const pdfParse = new PDFParse({ data: pdfBuffer });
|
||||
const pdfText = await pdfParse.getText();
|
||||
expect(pdfText.text).toContain('Column 1');
|
||||
expect(pdfText.text).toContain('Column 2');
|
||||
expect(pdfText.text).toContain('Column 3');
|
||||
});
|
||||
|
||||
test('it injects the correct language attribute into PDF export', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [randomDocFrench] = await createDoc(
|
||||
page,
|
||||
'doc-language-export-french',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await waitForLanguageSwitch(page, TestLanguage.French);
|
||||
|
||||
// Wait for the page to be ready after language switch
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
const header = page.locator('header').first();
|
||||
await header.locator('h1').getByText('Docs').click();
|
||||
|
||||
const randomDocFrench = randomName(
|
||||
'doc-language-export-french',
|
||||
browserName,
|
||||
1,
|
||||
)[0];
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Nouveau doc',
|
||||
})
|
||||
.click();
|
||||
|
||||
const input = page.getByRole('textbox', { name: 'Titre du document' });
|
||||
await expect(input).toBeVisible();
|
||||
await expect(input).toHaveText('', { timeout: 10000 });
|
||||
await input.click();
|
||||
await input.fill(randomDocFrench);
|
||||
await input.blur();
|
||||
|
||||
const editor = page.locator('.ProseMirror.bn-editor');
|
||||
await editor.click();
|
||||
await editor.fill('Contenu de test pour export en français');
|
||||
await writeInEditor({
|
||||
page,
|
||||
text: 'Contenu de test pour export en français',
|
||||
});
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
@@ -494,8 +438,8 @@ test.describe('Doc Export', () => {
|
||||
expect(download.suggestedFilename()).toBe(`${docChild}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfData = await pdf(pdfBuffer);
|
||||
|
||||
expect(pdfData.text).toContain(randomDoc);
|
||||
const pdfParse = new PDFParse({ data: pdfBuffer });
|
||||
const pdfText = await pdfParse.getText();
|
||||
expect(pdfText.text).toContain(randomDoc);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, getGridRow } from './utils-common';
|
||||
import { createDoc, getGridRow, verifyDocName } from './utils-common';
|
||||
import { addNewMember, connectOtherUserToDoc } from './utils-share';
|
||||
|
||||
type SmallDoc = {
|
||||
id: string;
|
||||
@@ -11,7 +12,7 @@ test.describe('Documents Grid mobile', () => {
|
||||
test.use({ viewport: { width: 500, height: 1200 } });
|
||||
|
||||
test('it checks the grid when mobile', async ({ page }) => {
|
||||
await page.route('**/documents/**', async (route) => {
|
||||
await page.route(/.*\/documents\/.*/, async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method().includes('GET') && request.url().includes('page=')) {
|
||||
await route.fulfill({
|
||||
@@ -133,6 +134,8 @@ test.describe('Document grid item options', () => {
|
||||
test('it deletes the document', async ({ page, browserName }) => {
|
||||
const [docTitle] = await createDoc(page, `delete doc`, browserName);
|
||||
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page.getByText(docTitle)).toBeVisible();
|
||||
@@ -161,7 +164,7 @@ test.describe('Document grid item options', () => {
|
||||
test("it checks if the delete option is disabled if we don't have the destroy capability", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route('*/**/api/v1.0/documents/?page=1', async (route) => {
|
||||
await page.route(/.*\/api\/v1.0\/documents\/\?page=1/, async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
results: [
|
||||
@@ -192,90 +195,68 @@ test.describe('Document grid item options', () => {
|
||||
});
|
||||
await page.goto('/');
|
||||
|
||||
const button = page.getByTestId(
|
||||
`docs-grid-actions-button-mocked-document-id`,
|
||||
);
|
||||
const button = page
|
||||
.getByTestId(`docs-grid-actions-button-mocked-document-id`)
|
||||
.first();
|
||||
await expect(button).toBeVisible();
|
||||
await button.click();
|
||||
const removeButton = page.getByTestId(
|
||||
`docs-grid-actions-remove-mocked-document-id`,
|
||||
);
|
||||
const removeButton = page
|
||||
.getByTestId(`docs-grid-actions-remove-mocked-document-id`)
|
||||
.first();
|
||||
await expect(removeButton).toBeVisible();
|
||||
await removeButton.isDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Documents filters', () => {
|
||||
test('it checks the prebuild left panel filters', async ({ page }) => {
|
||||
test('it checks the left panel filters', async ({ page, browserName }) => {
|
||||
void page.goto('/');
|
||||
|
||||
// Create my doc
|
||||
const [docName] = await createDoc(page, 'my-doc', browserName, 1);
|
||||
await verifyDocName(page, docName);
|
||||
|
||||
// Another user create a doc and share it with me
|
||||
const { cleanup, otherPage, otherBrowserName } =
|
||||
await connectOtherUserToDoc({
|
||||
browserName,
|
||||
docUrl: '/',
|
||||
});
|
||||
|
||||
const [docShareName] = await createDoc(
|
||||
otherPage,
|
||||
'my-share-doc',
|
||||
otherBrowserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await verifyDocName(otherPage, docShareName);
|
||||
|
||||
await otherPage.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
await addNewMember(otherPage, 0, 'Editor', browserName);
|
||||
|
||||
// Let's check the filters
|
||||
await page.getByRole('button', { name: 'Back to homepage' }).click();
|
||||
|
||||
const row = await getGridRow(page, docName);
|
||||
const rowShare = await getGridRow(page, docShareName);
|
||||
|
||||
// All Docs
|
||||
const response = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().endsWith('documents/?page=1') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
const result = await response.json();
|
||||
const allCount = result.count as number;
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
await expect(row).toBeVisible();
|
||||
await expect(rowShare).toBeVisible();
|
||||
|
||||
const allDocs = page.getByLabel('All docs');
|
||||
const myDocs = page.getByLabel('My docs');
|
||||
const sharedWithMe = page.getByLabel('Shared with me');
|
||||
|
||||
// Initial state
|
||||
await expect(allDocs).toBeVisible();
|
||||
await expect(allDocs).toHaveAttribute('aria-current', 'page');
|
||||
|
||||
await expect(myDocs).toBeVisible();
|
||||
await expect(myDocs).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
||||
await expect(myDocs).not.toHaveAttribute('aria-current');
|
||||
|
||||
await expect(sharedWithMe).toBeVisible();
|
||||
await expect(sharedWithMe).toHaveCSS(
|
||||
'background-color',
|
||||
'rgba(0, 0, 0, 0)',
|
||||
);
|
||||
await expect(sharedWithMe).not.toHaveAttribute('aria-current');
|
||||
|
||||
await allDocs.click();
|
||||
|
||||
await page.waitForURL('**/?target=all_docs');
|
||||
|
||||
let url = new URL(page.url());
|
||||
let target = url.searchParams.get('target');
|
||||
expect(target).toBe('all_docs');
|
||||
|
||||
// My docs
|
||||
await myDocs.click();
|
||||
url = new URL(page.url());
|
||||
target = url.searchParams.get('target');
|
||||
expect(target).toBe('my_docs');
|
||||
const responseMyDocs = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().endsWith('documents/?page=1&is_creator_me=true') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
const resultMyDocs = await responseMyDocs.json();
|
||||
const countMyDocs = resultMyDocs.count as number;
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
expect(countMyDocs).toBeLessThanOrEqual(allCount);
|
||||
// My Docs
|
||||
await page.getByRole('link', { name: 'My docs' }).click();
|
||||
await expect(row).toBeVisible();
|
||||
await expect(rowShare).toBeHidden();
|
||||
|
||||
// Shared with me
|
||||
await sharedWithMe.click();
|
||||
url = new URL(page.url());
|
||||
target = url.searchParams.get('target');
|
||||
expect(target).toBe('shared_with_me');
|
||||
const responseSharedWithMe = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('documents/?page=1&is_creator_me=false') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
const resultSharedWithMe = await responseSharedWithMe.json();
|
||||
const countSharedWithMe = resultSharedWithMe.count as number;
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
expect(countSharedWithMe).toBeLessThanOrEqual(allCount);
|
||||
expect(countSharedWithMe + countMyDocs).toEqual(allCount);
|
||||
await page.getByRole('link', { name: 'Shared with me' }).click();
|
||||
await expect(row).toBeHidden();
|
||||
await expect(rowShare).toBeVisible();
|
||||
|
||||
await cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
import { mockedAccesses, mockedInvitations } from './utils-share';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
import { createRootSubPage, getTreeRow } from './utils-sub-pages';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -65,25 +65,61 @@ test.describe('Doc Header', () => {
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await createDoc(page, 'doc-update', browserName, 1);
|
||||
await createDoc(page, 'doc-update-emoji', browserName, 1);
|
||||
|
||||
const emojiPicker = page.locator('.--docs--doc-title').getByRole('button');
|
||||
const optionMenu = page.getByLabel('Open the document options');
|
||||
const addEmojiMenuItem = page.getByRole('menuitem', { name: 'Add emoji' });
|
||||
const removeEmojiMenuItem = page.getByRole('menuitem', {
|
||||
name: 'Remove emoji',
|
||||
});
|
||||
|
||||
// Top parent should not have emoji picker
|
||||
await expect(emojiPicker).toBeHidden();
|
||||
await optionMenu.click();
|
||||
await expect(addEmojiMenuItem).toBeHidden();
|
||||
await expect(removeEmojiMenuItem).toBeHidden();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
const { name: docChild } = await createRootSubPage(
|
||||
page,
|
||||
browserName,
|
||||
'doc-update-emoji-child',
|
||||
);
|
||||
|
||||
await verifyDocName(page, docChild);
|
||||
|
||||
// Emoji picker should be hidden initially
|
||||
await expect(emojiPicker).toBeHidden();
|
||||
|
||||
// Add emoji
|
||||
await optionMenu.click();
|
||||
await expect(removeEmojiMenuItem).toBeHidden();
|
||||
await addEmojiMenuItem.click();
|
||||
await expect(emojiPicker).toHaveText('📄');
|
||||
|
||||
// Change emoji
|
||||
await emojiPicker.click({
|
||||
delay: 100,
|
||||
});
|
||||
await page.getByRole('button', { name: '😀' }).first().click();
|
||||
await expect(emojiPicker).toHaveText('😀');
|
||||
|
||||
// Update title
|
||||
const docTitle = page.getByRole('textbox', { name: 'Document title' });
|
||||
await expect(docTitle).toBeVisible();
|
||||
await docTitle.fill('👍 Hello Emoji World');
|
||||
await docTitle.fill('Hello Emoji World');
|
||||
await docTitle.blur();
|
||||
await verifyDocName(page, '👍 Hello Emoji World');
|
||||
await verifyDocName(page, 'Hello Emoji World');
|
||||
|
||||
// Check the tree
|
||||
const docTree = page.getByTestId('doc-tree');
|
||||
await expect(docTree.getByText('Hello Emoji World')).toBeVisible();
|
||||
await expect(docTree.getByTestId('doc-emoji-icon')).toBeVisible();
|
||||
await expect(docTree.getByTestId('doc-simple-icon')).toBeHidden();
|
||||
const row = await getTreeRow(page, 'Hello Emoji World');
|
||||
await expect(row.getByText('😀')).toBeVisible();
|
||||
|
||||
await page.getByTestId('home-button').click();
|
||||
|
||||
// Check the documents grid
|
||||
const gridRow = await getGridRow(page, 'Hello Emoji World');
|
||||
await expect(gridRow.getByTestId('doc-emoji-icon')).toBeVisible();
|
||||
await expect(gridRow.getByTestId('doc-simple-icon')).toBeHidden();
|
||||
// Remove emoji
|
||||
await optionMenu.click();
|
||||
await expect(addEmojiMenuItem).toBeHidden();
|
||||
await removeEmojiMenuItem.click();
|
||||
await expect(emojiPicker).toBeHidden();
|
||||
});
|
||||
|
||||
test('it deletes the doc', async ({ page, browserName }) => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
randomName,
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
import { writeInEditor } from './utils-editor';
|
||||
import { connectOtherUserToDoc, updateRoleUser } from './utils-share';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
@@ -240,11 +241,7 @@ test.describe('Document create member', () => {
|
||||
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
await page
|
||||
.locator('.ProseMirror')
|
||||
.locator('.bn-block-outer')
|
||||
.last()
|
||||
.fill('Hello World');
|
||||
await writeInEditor({ page, text: 'Hello World' });
|
||||
|
||||
const docUrl = page.url();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, goToGridDoc, verifyDocName } from './utils-common';
|
||||
import { createDoc, verifyDocName } from './utils-common';
|
||||
import { addNewMember } from './utils-share';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -8,8 +8,14 @@ test.beforeEach(async ({ page }) => {
|
||||
});
|
||||
|
||||
test.describe('Document list members', () => {
|
||||
test('it checks a big list of members', async ({ page }) => {
|
||||
const docTitle = await goToGridDoc(page);
|
||||
test('it checks a big list of members', async ({ page, browserName }) => {
|
||||
const [docTitle] = await createDoc(
|
||||
page,
|
||||
'members-big-members-list',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
// Get the current URL and extract the last part
|
||||
@@ -25,7 +31,7 @@ test.describe('Document list members', () => {
|
||||
return cleanUrl.split('/').pop() || '';
|
||||
})();
|
||||
|
||||
await page.route('**/documents/**/accesses/', async (route) => {
|
||||
await page.route(/.*\/documents\/.*\/accesses\//, async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
const pageId = url.searchParams.get('page') ?? '1';
|
||||
@@ -73,7 +79,7 @@ test.describe('Document list members', () => {
|
||||
await expect(loadMore).toBeHidden();
|
||||
});
|
||||
|
||||
test('it checks a big list of invitations', async ({ page }) => {
|
||||
test('it checks a big list of invitations', async ({ page, browserName }) => {
|
||||
await page.route(
|
||||
/.*\/documents\/.*\/invitations\/\?page=.*/,
|
||||
async (route) => {
|
||||
@@ -108,7 +114,12 @@ test.describe('Document list members', () => {
|
||||
},
|
||||
);
|
||||
|
||||
const docTitle = await goToGridDoc(page);
|
||||
const [docTitle] = await createDoc(
|
||||
page,
|
||||
'members-big-invitation-list',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
await verifyDocName(page, docTitle);
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
mockedDocument,
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
import { writeInEditor } from './utils-editor';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
test.describe('Doc Routing', () => {
|
||||
@@ -58,16 +59,23 @@ test.describe('Doc Routing', () => {
|
||||
|
||||
await createRootSubPage(page, browserName, '401-doc-child');
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
await writeInEditor({ page, text: 'Hello World' });
|
||||
|
||||
const responsePromise = page.route(
|
||||
/.*\/documents\/.*\/$|users\/me\/$/,
|
||||
async (route) => {
|
||||
const request = route.request();
|
||||
|
||||
// When we quit a document, a PATCH request is sent to save the document.
|
||||
// We intercept this request to simulate a 401 error from the backend.
|
||||
// The GET request to users/me is also intercepted to simulate the user
|
||||
// being logged out when trying to fetch user info.
|
||||
// This way we can test the 401 error handling when saving the document
|
||||
if (
|
||||
request.method().includes('PATCH') ||
|
||||
request.method().includes('GET')
|
||||
(request.url().includes('/documents/') &&
|
||||
request.method().includes('PATCH')) ||
|
||||
(request.url().includes('/users/me/') &&
|
||||
request.method().includes('GET'))
|
||||
) {
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
|
||||
@@ -102,6 +102,7 @@ test.describe('Doc Trashbin', () => {
|
||||
page,
|
||||
browserName,
|
||||
docParent: subDocName,
|
||||
docName: 'my-trash-editor-subsubdoc',
|
||||
});
|
||||
await verifyDocName(page, subsubDocName);
|
||||
|
||||
@@ -118,6 +119,10 @@ test.describe('Doc Trashbin', () => {
|
||||
await row.getByText(subDocName).click();
|
||||
await verifyDocName(page, subDocName);
|
||||
|
||||
await expect(
|
||||
page.locator('.--docs--editor-container.--docs--doc-deleted'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByLabel('Alert deleted document')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeDisabled();
|
||||
await expect(page.locator('.bn-editor')).toHaveAttribute(
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
import { addNewMember } from './utils-share';
|
||||
import { clickOnAddRootSubPage, createRootSubPage } from './utils-sub-pages';
|
||||
import {
|
||||
clickOnAddRootSubPage,
|
||||
createRootSubPage,
|
||||
getTreeRow,
|
||||
} from './utils-sub-pages';
|
||||
|
||||
test.describe('Doc Tree', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -298,6 +302,58 @@ test.describe('Doc Tree', () => {
|
||||
// Now test keyboard navigation on sub-document
|
||||
await expect(docTree.getByText(docChild)).toBeVisible();
|
||||
});
|
||||
|
||||
test('it updates the child icon from the tree', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [docParent] = await createDoc(
|
||||
page,
|
||||
'doc-child-emoji',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
await verifyDocName(page, docParent);
|
||||
|
||||
const { name: docChild } = await createRootSubPage(
|
||||
page,
|
||||
browserName,
|
||||
'doc-child-emoji-child',
|
||||
);
|
||||
|
||||
const row = await getTreeRow(page, docChild);
|
||||
|
||||
// Check Remove emoji is not present initially
|
||||
await row.hover();
|
||||
const menu = row.getByText(`more_horiz`);
|
||||
await menu.click();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Remove emoji' }),
|
||||
).toBeHidden();
|
||||
|
||||
// Close the menu
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// Update the emoji from the tree
|
||||
await row.locator('.--docs--doc-icon').click();
|
||||
await page.getByRole('button', { name: '😀' }).first().click();
|
||||
|
||||
// Verify the emoji is updated in the tree and in the document title
|
||||
await expect(row.getByText('😀')).toBeVisible();
|
||||
|
||||
const titleEmojiPicker = page
|
||||
.locator('.--docs--doc-title')
|
||||
.getByRole('button');
|
||||
await expect(titleEmojiPicker).toHaveText('😀');
|
||||
|
||||
// Now remove the emoji using the new action
|
||||
await row.hover();
|
||||
await menu.click();
|
||||
await page.getByRole('menuitem', { name: 'Remove emoji' }).click();
|
||||
|
||||
await expect(row.getByText('😀')).toBeHidden();
|
||||
await expect(titleEmojiPicker).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Doc Tree: Inheritance', () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
keyCloakSignIn,
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
import { getEditor, writeInEditor } from './utils-editor';
|
||||
import { addNewMember, connectOtherUserToDoc } from './utils-share';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
@@ -151,18 +152,15 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
await page
|
||||
.locator('.ProseMirror')
|
||||
.locator('.bn-block-outer')
|
||||
.last()
|
||||
.fill('Hello World');
|
||||
await writeInEditor({ page, text: 'Hello World' });
|
||||
|
||||
const docUrl = page.url();
|
||||
|
||||
const { otherBrowserName, otherPage } = await connectOtherUserToDoc({
|
||||
browserName,
|
||||
docUrl,
|
||||
});
|
||||
const { otherBrowserName, otherPage, cleanup } =
|
||||
await connectOtherUserToDoc({
|
||||
browserName,
|
||||
docUrl,
|
||||
});
|
||||
|
||||
await expect(
|
||||
otherPage.getByText('Insufficient access rights to view the document.'),
|
||||
@@ -175,20 +173,23 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
await addNewMember(page, 0, 'Reader', otherBrowserName);
|
||||
|
||||
await otherPage.reload();
|
||||
await expect(otherPage.getByText('Hello World')).toBeVisible();
|
||||
await expect(otherPage.getByText('Hello World')).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
await cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Doc Visibility: Public', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('It checks a public doc in read only mode', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
|
||||
const [docTitle] = await createDoc(
|
||||
page,
|
||||
'Public read only',
|
||||
@@ -198,6 +199,8 @@ test.describe('Doc Visibility: Public', () => {
|
||||
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
await writeInEditor({ page, text: 'Hello Public Viewonly' });
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
const selectVisibility = page.getByTestId('doc-visibility');
|
||||
await selectVisibility.click();
|
||||
@@ -239,49 +242,67 @@ test.describe('Doc Visibility: Public', () => {
|
||||
await expect(page.getByTestId('search-docs-button')).toBeVisible();
|
||||
await expect(page.getByTestId('new-doc-button')).toBeVisible();
|
||||
|
||||
const urlDoc = page.url();
|
||||
const docUrl = page.url();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Logout',
|
||||
})
|
||||
.click();
|
||||
const { otherPage, cleanup } = await connectOtherUserToDoc({
|
||||
browserName,
|
||||
docUrl,
|
||||
withoutSignIn: true,
|
||||
});
|
||||
|
||||
await expectLoginPage(page);
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||
await expect(page.getByTestId('search-docs-button')).toBeHidden();
|
||||
await expect(page.getByTestId('new-doc-button')).toBeHidden();
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
||||
const card = page.getByLabel('It is the card information');
|
||||
await expect(otherPage.locator('h2').getByText(docTitle)).toBeVisible();
|
||||
await expect(otherPage.getByTestId('search-docs-button')).toBeHidden();
|
||||
await expect(otherPage.getByTestId('new-doc-button')).toBeHidden();
|
||||
await expect(
|
||||
otherPage.getByRole('button', { name: 'Share' }),
|
||||
).toBeVisible();
|
||||
const card = otherPage.getByLabel('It is the card information');
|
||||
await expect(card).toBeVisible();
|
||||
await expect(card.getByText('Reader')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
await expect(
|
||||
page.getByText(
|
||||
otherPage.locator('.--docs--editor-container.--docs--doc-readonly'),
|
||||
).toBeVisible();
|
||||
|
||||
const otherEditor = await getEditor({ page: otherPage });
|
||||
await expect(otherEditor).toHaveAttribute('contenteditable', 'false');
|
||||
await expect(otherEditor.getByText('Hello Public Viewonly')).toBeVisible();
|
||||
|
||||
// Cursor and selection of the anonymous user are not visible
|
||||
await otherEditor.getByText('Hello Public').selectText();
|
||||
await expect(
|
||||
page.locator('.collaboration-cursor-custom__base'),
|
||||
).toBeHidden();
|
||||
await expect(page.locator('.ProseMirror-yjs-selection')).toBeHidden();
|
||||
|
||||
// Can still see changes made by others
|
||||
await writeInEditor({ page, text: 'Can you see it ?' });
|
||||
await expect(otherEditor.getByText('Can you see it ?')).toBeVisible();
|
||||
|
||||
await otherPage.getByRole('button', { name: 'Share' }).click();
|
||||
await expect(
|
||||
otherPage.getByText(
|
||||
'You can view this document but need additional access to see its members or modify settings.',
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Request access' }),
|
||||
otherPage.getByRole('button', { name: 'Request access' }),
|
||||
).toBeHidden();
|
||||
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
test('It checks a public doc in editable mode', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
|
||||
const [docTitle] = await createDoc(page, 'Public editable', browserName, 1);
|
||||
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
await writeInEditor({ page, text: 'Hello Public Editable' });
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
const selectVisibility = page.getByTestId('doc-visibility');
|
||||
await selectVisibility.click();
|
||||
@@ -315,20 +336,47 @@ test.describe('Doc Visibility: Public', () => {
|
||||
cardContainer.getByText('Public document', { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
const urlDoc = page.url();
|
||||
const docUrl = page.url();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Logout',
|
||||
})
|
||||
.click();
|
||||
const { otherPage, cleanup } = await connectOtherUserToDoc({
|
||||
browserName,
|
||||
docUrl,
|
||||
withoutSignIn: true,
|
||||
docTitle,
|
||||
});
|
||||
|
||||
await expectLoginPage(page);
|
||||
await expect(otherPage.getByTestId('search-docs-button')).toBeHidden();
|
||||
await expect(otherPage.getByTestId('new-doc-button')).toBeHidden();
|
||||
|
||||
await page.goto(urlDoc);
|
||||
const otherEditor = await getEditor({ page: otherPage });
|
||||
await expect(otherEditor).toHaveAttribute('contenteditable', 'true');
|
||||
await expect(otherEditor.getByText('Hello Public Editable')).toBeVisible();
|
||||
|
||||
await verifyDocName(page, docTitle);
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
||||
// We can see the collaboration cursor of the anonymous user
|
||||
await otherEditor.getByText('Hello Public').selectText();
|
||||
await expect(
|
||||
page.locator('.collaboration-cursor-custom__base').getByText('Anonymous'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
otherPage.getByRole('button', { name: 'Share' }),
|
||||
).toBeVisible();
|
||||
const card = otherPage.getByLabel('It is the card information');
|
||||
await expect(card).toBeVisible();
|
||||
await expect(card.getByText('Editor')).toBeVisible();
|
||||
|
||||
await otherPage.getByRole('button', { name: 'Share' }).click();
|
||||
await expect(
|
||||
otherPage.getByText(
|
||||
'You can view this document but need additional access to see its members or modify settings.',
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
otherPage.getByRole('button', { name: 'Request access' }),
|
||||
).toBeHidden();
|
||||
|
||||
await cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ test.describe('Header: Override configuration', () => {
|
||||
logo: {
|
||||
src: '/assets/logo-gouv.svg',
|
||||
width: '220px',
|
||||
alt: 'Gouvernement Logo',
|
||||
alt: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -168,8 +168,11 @@ test.describe('Header: Override configuration', () => {
|
||||
await page.goto('/');
|
||||
const header = page.locator('header').first();
|
||||
|
||||
await expect(header.getByAltText('Gouvernement Logo')).toBeVisible();
|
||||
const logoImage = header.getByTestId('header-icon-docs');
|
||||
await expect(logoImage).toBeVisible();
|
||||
|
||||
await expect(header.getByAltText('Docs')).toBeHidden();
|
||||
await expect(logoImage).not.toHaveAttribute('src', '/assets/icon-docs.svg');
|
||||
await expect(logoImage).toHaveAttribute('src', '/assets/logo-gouv.svg');
|
||||
await expect(logoImage).toHaveAttribute('alt', '');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { TestLanguage, createDoc, waitForLanguageSwitch } from './utils-common';
|
||||
import { openSuggestionMenu } from './utils-editor';
|
||||
|
||||
test.describe('Language', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -51,6 +52,7 @@ test.describe('Language', () => {
|
||||
await expect(page.locator('html')).toHaveAttribute('lang', 'en');
|
||||
await expect(languagePicker).toContainText('English');
|
||||
});
|
||||
|
||||
test('can switch language using only keyboard', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForLanguageSwitch(page, TestLanguage.English);
|
||||
@@ -106,18 +108,18 @@ test.describe('Language', () => {
|
||||
}) => {
|
||||
await createDoc(page, 'doc-toolbar', browserName, 1);
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
|
||||
// Trigger slash menu to show english menu
|
||||
await editor.click();
|
||||
await editor.fill('/');
|
||||
const editor = await openSuggestionMenu({ page });
|
||||
await expect(page.getByText('Headings', { exact: true })).toBeVisible();
|
||||
|
||||
await editor.click(); // close the menu
|
||||
|
||||
await expect(page.getByText('Headings', { exact: true })).toBeHidden();
|
||||
|
||||
// Change language to French
|
||||
await waitForLanguageSwitch(page, TestLanguage.French);
|
||||
|
||||
// Trigger slash menu to show french menu
|
||||
await editor.locator('.bn-block-outer').last().fill('/');
|
||||
await openSuggestionMenu({ page });
|
||||
await expect(page.getByText('Titres', { exact: true })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc } from './utils-common';
|
||||
|
||||
test.describe('Left panel desktop', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -11,6 +13,53 @@ test.describe('Left panel desktop', () => {
|
||||
await expect(page.getByTestId('home-button')).toBeVisible();
|
||||
await expect(page.getByTestId('new-doc-button')).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks resize handle is present and functional on document page', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
// On home page, resize handle should NOT be present
|
||||
let resizeHandle = page.locator('[data-panel-resize-handle-id]');
|
||||
await expect(resizeHandle).toBeHidden();
|
||||
|
||||
// Create and navigate to a document
|
||||
await createDoc(page, 'doc-resize-test', browserName, 1);
|
||||
|
||||
// Now resize handle should be visible on document page
|
||||
resizeHandle = page.locator('[data-panel-resize-handle-id]').first();
|
||||
await expect(resizeHandle).toBeVisible();
|
||||
|
||||
const leftPanel = page.getByTestId('left-panel-desktop');
|
||||
await expect(leftPanel).toBeVisible();
|
||||
|
||||
// Get initial panel width
|
||||
const initialBox = await leftPanel.boundingBox();
|
||||
expect(initialBox).not.toBeNull();
|
||||
|
||||
// Get handle position
|
||||
const handleBox = await resizeHandle.boundingBox();
|
||||
expect(handleBox).not.toBeNull();
|
||||
|
||||
// Test resize by dragging the handle
|
||||
await page.mouse.move(
|
||||
handleBox!.x + handleBox!.width / 2,
|
||||
handleBox!.y + handleBox!.height / 2,
|
||||
);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(
|
||||
handleBox!.x + 100,
|
||||
handleBox!.y + handleBox!.height / 2,
|
||||
);
|
||||
await page.mouse.up();
|
||||
|
||||
// Wait for resize to complete
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Verify the panel has been resized
|
||||
const newBox = await leftPanel.boundingBox();
|
||||
expect(newBox).not.toBeNull();
|
||||
expect(newBox!.width).toBeGreaterThan(initialBox!.width);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Left panel mobile', () => {
|
||||
@@ -47,4 +96,12 @@ test.describe('Left panel mobile', () => {
|
||||
await expect(languageButton).toBeInViewport();
|
||||
await expect(logoutButton).toBeInViewport();
|
||||
});
|
||||
|
||||
test('checks resize handle is not present on mobile', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Verify the resize handle is NOT present on mobile
|
||||
const resizeHandle = page.locator('[data-panel-resize-handle-id]');
|
||||
await expect(resizeHandle).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ export const overrideConfig = async (
|
||||
page: Page,
|
||||
newConfig: { [_K in keyof typeof CONFIG]?: unknown },
|
||||
) =>
|
||||
await page.route('**/api/v1.0/config/', async (route) => {
|
||||
await page.route(/.*\/api\/v1.0\/config\/.*/, async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method().includes('GET')) {
|
||||
await route.fulfill({
|
||||
@@ -48,7 +48,7 @@ export const overrideConfig = async (
|
||||
export const keyCloakSignIn = async (
|
||||
page: Page,
|
||||
browserName: string,
|
||||
fromHome: boolean = true,
|
||||
fromHome = true,
|
||||
) => {
|
||||
if (fromHome) {
|
||||
await page.getByRole('button', { name: 'Start Writing' }).first().click();
|
||||
@@ -79,8 +79,8 @@ export const createDoc = async (
|
||||
page: Page,
|
||||
docName: string,
|
||||
browserName: string,
|
||||
length: number = 1,
|
||||
isMobile: boolean = false,
|
||||
length = 1,
|
||||
isMobile = false,
|
||||
) => {
|
||||
const randomDocs = randomName(docName, browserName, length);
|
||||
|
||||
@@ -204,7 +204,7 @@ export const waitForResponseCreateDoc = (page: Page) => {
|
||||
};
|
||||
|
||||
export const mockedDocument = async (page: Page, data: object) => {
|
||||
await page.route('**/documents/**/', async (route) => {
|
||||
await page.route(/\**\/documents\/\**/, async (route) => {
|
||||
const request = route.request();
|
||||
if (
|
||||
request.method().includes('GET') &&
|
||||
@@ -256,7 +256,7 @@ export const mockedDocument = async (page: Page, data: object) => {
|
||||
};
|
||||
|
||||
export const mockedListDocs = async (page: Page, data: object[] = []) => {
|
||||
await page.route('**/documents/**/', async (route) => {
|
||||
await page.route(/\**\/documents\/\**/, async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method().includes('GET') && request.url().includes('page=')) {
|
||||
await route.fulfill({
|
||||
@@ -277,6 +277,7 @@ export const expectLoginPage = async (page: Page) =>
|
||||
).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// language helper
|
||||
export const TestLanguage = {
|
||||
English: {
|
||||
@@ -300,7 +301,7 @@ export async function waitForLanguageSwitch(
|
||||
page: Page,
|
||||
lang: TestLanguageValue,
|
||||
) {
|
||||
await page.route('**/api/v1.0/users/**', async (route, request) => {
|
||||
await page.route(/\**\/api\/v1.0\/users\/\**/, async (route, request) => {
|
||||
if (request.method().includes('PATCH')) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
|
||||
@@ -9,7 +9,7 @@ export const getEditor = async ({ page }: { page: Page }) => {
|
||||
export const openSuggestionMenu = async ({ page }: { page: Page }) => {
|
||||
const editor = await getEditor({ page });
|
||||
await editor.click();
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await writeInEditor({ page, text: '/' });
|
||||
|
||||
return editor;
|
||||
};
|
||||
@@ -22,6 +22,6 @@ export const writeInEditor = async ({
|
||||
text: string;
|
||||
}) => {
|
||||
const editor = await getEditor({ page });
|
||||
editor.locator('.bn-block-outer').last().fill(text);
|
||||
await editor.locator('.bn-block-outer .bn-inline-content').last().fill(text);
|
||||
return editor;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export const addNewMember = async (
|
||||
page: Page,
|
||||
index: number,
|
||||
role: 'Administrator' | 'Owner' | 'Editor' | 'Reader',
|
||||
fillText: string = 'user.test',
|
||||
fillText = 'user.test',
|
||||
) => {
|
||||
const responsePromiseSearchUser = page.waitForResponse(
|
||||
(response) =>
|
||||
@@ -118,12 +118,16 @@ export const connectOtherUserToDoc = async ({
|
||||
await otherPage.goto(docUrl);
|
||||
|
||||
if (!withoutSignIn) {
|
||||
await otherPage
|
||||
const loginFromApp = otherPage
|
||||
.getByRole('main', { name: 'Main content' })
|
||||
.getByLabel('Login')
|
||||
.click({
|
||||
timeout: 15000,
|
||||
});
|
||||
.getByLabel('Login');
|
||||
const loginFromHome = otherPage.getByRole('button', {
|
||||
name: 'Start Writing',
|
||||
});
|
||||
|
||||
await loginFromApp.or(loginFromHome).first().click({
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
await keyCloakSignIn(otherPage, otherBrowserName, false);
|
||||
}
|
||||
@@ -159,7 +163,7 @@ export const mockedInvitations = async (page: Page, json?: object) => {
|
||||
...json,
|
||||
},
|
||||
];
|
||||
await page.route('**/invitations/**/', async (route) => {
|
||||
await page.route(/.*\/invitations\/.*/, async (route) => {
|
||||
const request = route.request();
|
||||
if (
|
||||
request.method().includes('GET') &&
|
||||
@@ -180,7 +184,7 @@ export const mockedInvitations = async (page: Page, json?: object) => {
|
||||
});
|
||||
|
||||
await page.route(
|
||||
'**/invitations/120ec765-43af-4602-83eb-7f4e1224548a/**/',
|
||||
/.*\/invitations\/120ec765-43af-4602-83eb-7f4e1224548a\/.*/,
|
||||
async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method().includes('DELETE')) {
|
||||
@@ -195,7 +199,7 @@ export const mockedInvitations = async (page: Page, json?: object) => {
|
||||
};
|
||||
|
||||
export const mockedAccesses = async (page: Page, json?: object) => {
|
||||
await page.route('**/accesses/**/', async (route) => {
|
||||
await page.route(/.*\/accesses\/.*/, async (route) => {
|
||||
const request = route.request();
|
||||
|
||||
if (
|
||||
|
||||
@@ -12,7 +12,7 @@ export const createRootSubPage = async (
|
||||
page: Page,
|
||||
browserName: BrowserName,
|
||||
docName: string,
|
||||
isMobile: boolean = false,
|
||||
isMobile = false,
|
||||
) => {
|
||||
if (isMobile) {
|
||||
await page
|
||||
@@ -72,10 +72,12 @@ export const addChild = async ({
|
||||
page,
|
||||
browserName,
|
||||
docParent,
|
||||
docName,
|
||||
}: {
|
||||
page: Page;
|
||||
browserName: BrowserName;
|
||||
docParent: string;
|
||||
docName: string;
|
||||
}) => {
|
||||
let item = page.getByTestId('doc-tree-root-item');
|
||||
|
||||
@@ -99,12 +101,26 @@ export const addChild = async ({
|
||||
await item.hover();
|
||||
await item.getByTestId('doc-tree-item-actions-add-child').click();
|
||||
|
||||
const [name] = randomName(docParent, browserName, 1);
|
||||
const [name] = randomName(docName, browserName, 1);
|
||||
await updateDocTitle(page, name);
|
||||
|
||||
return name;
|
||||
};
|
||||
|
||||
export const getTreeRow = async (page: Page, title: string) => {
|
||||
const docTree = page.getByTestId('doc-tree');
|
||||
const row = docTree
|
||||
.getByRole('treeitem')
|
||||
.filter({
|
||||
hasText: title,
|
||||
})
|
||||
.first();
|
||||
|
||||
await expect(row).toBeVisible();
|
||||
|
||||
return row;
|
||||
};
|
||||
|
||||
export const navigateToTopParentFromTree = async ({ page }: { page: Page }) => {
|
||||
await page.getByRole('link', { name: /Open root document/ }).click();
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "3.8.2",
|
||||
"version": "3.9.0",
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"author": "DINUM",
|
||||
"license": "MIT",
|
||||
@@ -15,7 +15,7 @@
|
||||
"test:ui::chromium": "yarn test:ui --project=chromium"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.55.0",
|
||||
"@playwright/test": "1.56.1",
|
||||
"@types/node": "*",
|
||||
"@types/pdf-parse": "1.1.5",
|
||||
"eslint-plugin-docs": "*",
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"convert-stream": "1.0.2",
|
||||
"pdf-parse": "1.1.1"
|
||||
"pdf-parse": "2.4.5"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "3.8.2",
|
||||
"version": "3.9.0",
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"author": "DINUM",
|
||||
"license": "MIT",
|
||||
@@ -19,47 +19,51 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ag-media/react-pdf-table": "2.0.3",
|
||||
"@blocknote/code-block": "0.37.0",
|
||||
"@blocknote/core": "0.37.0",
|
||||
"@blocknote/mantine": "0.37.0",
|
||||
"@blocknote/react": "0.37.0",
|
||||
"@blocknote/xl-docx-exporter": "0.37.0",
|
||||
"@blocknote/xl-multi-column": "0.37.0",
|
||||
"@blocknote/xl-pdf-exporter": "0.37.0",
|
||||
"@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",
|
||||
"@dnd-kit/core": "6.3.1",
|
||||
"@dnd-kit/modifiers": "9.0.0",
|
||||
"@emoji-mart/data": "1.2.1",
|
||||
"@emoji-mart/react": "1.1.1",
|
||||
"@fontsource-variable/inter": "5.2.8",
|
||||
"@fontsource-variable/material-symbols-outlined": "5.2.25",
|
||||
"@fontsource/material-icons": "5.2.5",
|
||||
"@fontsource-variable/material-symbols-outlined": "5.2.28",
|
||||
"@fontsource/material-icons": "5.2.7",
|
||||
"@gouvfr-lasuite/integration": "1.0.3",
|
||||
"@gouvfr-lasuite/ui-kit": "0.16.1",
|
||||
"@hocuspocus/provider": "2.15.2",
|
||||
"@gouvfr-lasuite/ui-kit": "0.16.2",
|
||||
"@hocuspocus/provider": "3.4.0",
|
||||
"@mantine/core": "8.3.6",
|
||||
"@mantine/hooks": "8.3.6",
|
||||
"@openfun/cunningham-react": "3.2.3",
|
||||
"@react-pdf/renderer": "4.3.0",
|
||||
"@sentry/nextjs": "10.11.0",
|
||||
"@tanstack/react-query": "5.87.4",
|
||||
"@react-pdf/renderer": "4.3.1",
|
||||
"@sentry/nextjs": "10.22.0",
|
||||
"@tanstack/react-query": "5.90.6",
|
||||
"@tiptap/extensions": "3.10.1",
|
||||
"canvg": "4.0.3",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"docx": "9.5.0",
|
||||
"crisp-sdk-web": "1.0.26",
|
||||
"docx": "*",
|
||||
"emoji-datasource-apple": "16.0.0",
|
||||
"emoji-mart": "5.6.0",
|
||||
"emoji-regex": "10.5.0",
|
||||
"i18next": "25.5.2",
|
||||
"emoji-regex": "10.6.0",
|
||||
"i18next": "25.6.0",
|
||||
"i18next-browser-languagedetector": "8.2.0",
|
||||
"idb": "8.0.3",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.7.2",
|
||||
"next": "15.5.3",
|
||||
"posthog-js": "1.264.2",
|
||||
"next": "15.5.4",
|
||||
"posthog-js": "1.284.0",
|
||||
"react": "*",
|
||||
"react-aria-components": "1.12.1",
|
||||
"react-aria-components": "1.13.0",
|
||||
"react-dom": "*",
|
||||
"react-i18next": "15.7.3",
|
||||
"react-intersection-observer": "9.16.0",
|
||||
"react-i18next": "16.2.3",
|
||||
"react-intersection-observer": "10.0.0",
|
||||
"react-resizable-panels": "3.0.6",
|
||||
"react-select": "5.10.2",
|
||||
"styled-components": "6.1.19",
|
||||
"use-debounce": "10.0.6",
|
||||
@@ -69,9 +73,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@tanstack/react-query-devtools": "5.87.4",
|
||||
"@tanstack/react-query-devtools": "5.90.2",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/jest-dom": "6.8.0",
|
||||
"@testing-library/jest-dom": "6.9.1",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/lodash": "4.17.20",
|
||||
@@ -79,22 +83,22 @@
|
||||
"@types/node": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"@vitejs/plugin-react": "5.0.2",
|
||||
"@vitejs/plugin-react": "5.1.0",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"cross-env": "10.0.0",
|
||||
"dotenv": "17.2.2",
|
||||
"cross-env": "10.1.0",
|
||||
"dotenv": "17.2.3",
|
||||
"eslint-plugin-docs": "*",
|
||||
"fetch-mock": "9.11.0",
|
||||
"jsdom": "26.1.0",
|
||||
"jsdom": "27.1.0",
|
||||
"node-fetch": "2.7.0",
|
||||
"prettier": "3.6.2",
|
||||
"stylelint": "16.24.0",
|
||||
"stylelint-config-standard": "39.0.0",
|
||||
"stylelint": "16.25.0",
|
||||
"stylelint-config-standard": "39.0.1",
|
||||
"stylelint-prettier": "5.0.3",
|
||||
"typescript": "*",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.2.4",
|
||||
"webpack": "5.101.3",
|
||||
"vitest": "4.0.6",
|
||||
"webpack": "5.102.1",
|
||||
"workbox-webpack-plugin": "7.1.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
|
||||
@@ -18,5 +18,5 @@ export const backendUrl = () =>
|
||||
* @param apiVersion - The version of the API (defaults to '1.0').
|
||||
* @returns The full versioned API base URL as a string.
|
||||
*/
|
||||
export const baseApiUrl = (apiVersion: string = '1.0') =>
|
||||
export const baseApiUrl = (apiVersion = '1.0') =>
|
||||
`${backendUrl()}/api/v${apiVersion}/`;
|
||||
|
||||
@@ -31,7 +31,7 @@ const StyledButton = styled(Button)<StyledButtonProps>`
|
||||
font-weight: 500;
|
||||
font-size: 0.938rem;
|
||||
padding: 0;
|
||||
${({ $css }) => $css};
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
background-color: var(
|
||||
--c--components--button--primary-text--background--color-hover
|
||||
@@ -41,6 +41,7 @@ const StyledButton = styled(Button)<StyledButtonProps>`
|
||||
box-shadow: 0 0 0 2px var(--c--theme--colors--primary-400);
|
||||
border-radius: 4px;
|
||||
}
|
||||
${({ $css }) => $css};
|
||||
`;
|
||||
|
||||
export interface DropButtonProps {
|
||||
|
||||
@@ -13,7 +13,7 @@ export const Icon = ({
|
||||
iconName,
|
||||
disabled,
|
||||
variant = 'outlined',
|
||||
$variation,
|
||||
$variation = 'text',
|
||||
...textProps
|
||||
}: IconProps) => {
|
||||
const hasLabel = 'aria-label' in textProps || 'aria-labelledby' in textProps;
|
||||
@@ -41,15 +41,19 @@ type IconOptionsProps = TextType & {
|
||||
isHorizontal?: boolean;
|
||||
};
|
||||
|
||||
export const IconOptions = ({ isHorizontal, ...props }: IconOptionsProps) => {
|
||||
export const IconOptions = ({
|
||||
isHorizontal,
|
||||
$css,
|
||||
...props
|
||||
}: IconOptionsProps) => {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
iconName={isHorizontal ? 'more_horiz' : 'more_vert'}
|
||||
$css={css`
|
||||
user-select: none;
|
||||
${props.$css}
|
||||
${$css}
|
||||
`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,6 +44,11 @@
|
||||
contain: content;
|
||||
}
|
||||
|
||||
.c__button--medium {
|
||||
min-height: var(--c--components--button--medium-height);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal
|
||||
*/
|
||||
|
||||
@@ -44,7 +44,7 @@ export function useAuthQuery(
|
||||
staleTime: 1000 * 60 * 15, // 15 minutes
|
||||
retry: (failureCount, error) => {
|
||||
// we assume that a 401 means the user is not logged in
|
||||
if (error.status == 401) {
|
||||
if (error.status === 401) {
|
||||
return false;
|
||||
}
|
||||
return failureCount < DEFAULT_QUERY_RETRY;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { baseApiUrl } from '@/api';
|
||||
|
||||
export const HOME_URL: string = '/home';
|
||||
export const HOME_URL = '/home';
|
||||
export const LOGIN_URL = `${baseApiUrl()}authenticate/`;
|
||||
export const LOGOUT_URL = `${baseApiUrl()}logout/`;
|
||||
export const PATH_AUTH_LOCAL_STORAGE = 'docs-path-auth';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { APIError, errorCauses } from '@/api';
|
||||
import { sleep } from '@/utils';
|
||||
|
||||
interface CheckDocMediaStatusResponse {
|
||||
file?: string;
|
||||
@@ -25,3 +26,28 @@ export const checkDocMediaStatus = async ({
|
||||
|
||||
return response.json() as Promise<CheckDocMediaStatusResponse>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload file can be analyzed on the server side,
|
||||
* we had this function to wait for the analysis to be done
|
||||
* before returning the file url. It will keep the loader
|
||||
* on the upload button until the analysis is done.
|
||||
* @param url
|
||||
* @returns Promise<CheckDocMediaStatusResponse> status_code
|
||||
* @description Waits for the upload to be analyzed by checking the status of the file.
|
||||
*/
|
||||
export const loopCheckDocMediaStatus = async (
|
||||
url: string,
|
||||
): Promise<CheckDocMediaStatusResponse> => {
|
||||
const SLEEP_TIME = 5000;
|
||||
const response = await checkDocMediaStatus({
|
||||
urlMedia: url,
|
||||
});
|
||||
|
||||
if (response.status === 'ready') {
|
||||
return response;
|
||||
} else {
|
||||
await sleep(SLEEP_TIME);
|
||||
return await loopCheckDocMediaStatus(url);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
<svg width="18" height="23" viewBox="0 0 18 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.57954 5.93282C4.44486 5.79021 4.37793 5.61484 4.37793 5.41299C4.37793 5.21112 4.44495 5.03795 4.58154 4.90136C4.72456 4.75833 4.90437 4.6875 5.11367 4.6875H12.8964C13.0991 4.6875 13.2722 4.75857 13.408 4.90133C13.5508 5.03718 13.6219 5.21027 13.6219 5.41299C13.6219 5.61613 13.5506 5.7918 13.409 5.93387C13.273 6.07732 13.0996 6.14873 12.8964 6.14873H5.11367C4.90437 6.14873 4.72456 6.0779 4.58154 5.93487L4.57954 5.93282ZM4.57954 9.51144C4.44486 9.36882 4.37793 9.19346 4.37793 8.9916C4.37793 8.78973 4.44495 8.61656 4.58154 8.47997C4.72456 8.33695 4.90437 8.26611 5.11367 8.26611H12.8964C13.0991 8.26611 13.2722 8.33719 13.408 8.47995C13.5508 8.61579 13.6219 8.78888 13.6219 8.9916C13.6219 9.19475 13.5506 9.37042 13.409 9.51249C13.273 9.65593 13.0996 9.72734 12.8964 9.72734H5.11367C4.90437 9.72734 4.72456 9.65651 4.58154 9.51348L4.57954 9.51144ZM4.57954 13.1003C4.44561 12.9585 4.37793 12.7869 4.37793 12.5907C4.37793 12.3831 4.44414 12.204 4.57954 12.0606L4.58151 12.0586C4.72453 11.9155 4.90437 11.8447 5.11367 11.8447H8.79482C9.00363 11.8447 9.18092 11.9153 9.3177 12.0596C9.46006 12.2024 9.53057 12.3819 9.53057 12.5907C9.53057 12.7887 9.45812 12.9609 9.31671 13.1024C9.17936 13.2397 9.00235 13.306 8.79482 13.306H5.11367C4.90609 13.306 4.72695 13.2397 4.58358 13.1043L4.57954 13.1003ZM1.09476 0.851519C1.65317 0.285946 2.47955 0.0117188 3.55508 0.0117188H14.4447C15.52 0.0117188 16.3433 0.28583 16.895 0.851748C17.4529 1.41698 17.7234 2.24966 17.7234 3.33145V18.8866C17.7234 19.975 17.4531 20.8082 16.8945 21.3668C16.3427 21.9256 15.5196 22.1961 14.4447 22.1961H3.55508C2.47988 22.1961 1.65367 21.9255 1.09521 21.367C0.543652 20.8083 0.276367 19.9747 0.276367 18.8866V3.33145C0.276367 2.24984 0.543796 1.41679 1.09476 0.851519ZM15.5624 20.0351C15.2958 20.3085 14.8959 20.4452 14.3627 20.4452H3.63711C3.10391 20.4452 2.70059 20.3085 2.42715 20.0351L2.49875 19.9652L2.49786 19.9643L2.42715 20.0351C2.16055 19.7616 2.02725 19.3686 2.02725 18.8559V3.36221C2.02725 2.84951 2.16055 2.45645 2.42715 2.18301C2.70059 1.90273 3.10391 1.7626 3.63711 1.7626H14.3627C14.8959 1.7626 15.2958 1.90273 15.5624 2.18301C15.8358 2.45645 15.9726 2.84951 15.9726 3.36221V18.8559C15.9726 19.3686 15.8358 19.7616 15.5624 20.0351Z" fill="#3A3A3A"/>
|
||||
<svg viewBox="0 0 18 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.57954 5.93282C4.44486 5.79021 4.37793 5.61484 4.37793 5.41299C4.37793 5.21112 4.44495 5.03795 4.58154 4.90136C4.72456 4.75833 4.90437 4.6875 5.11367 4.6875H12.8964C13.0991 4.6875 13.2722 4.75857 13.408 4.90133C13.5508 5.03718 13.6219 5.21027 13.6219 5.41299C13.6219 5.61613 13.5506 5.7918 13.409 5.93387C13.273 6.07732 13.0996 6.14873 12.8964 6.14873H5.11367C4.90437 6.14873 4.72456 6.0779 4.58154 5.93487L4.57954 5.93282ZM4.57954 9.51144C4.44486 9.36882 4.37793 9.19346 4.37793 8.9916C4.37793 8.78973 4.44495 8.61656 4.58154 8.47997C4.72456 8.33695 4.90437 8.26611 5.11367 8.26611H12.8964C13.0991 8.26611 13.2722 8.33719 13.408 8.47995C13.5508 8.61579 13.6219 8.78888 13.6219 8.9916C13.6219 9.19475 13.5506 9.37042 13.409 9.51249C13.273 9.65593 13.0996 9.72734 12.8964 9.72734H5.11367C4.90437 9.72734 4.72456 9.65651 4.58154 9.51348L4.57954 9.51144ZM4.57954 13.1003C4.44561 12.9585 4.37793 12.7869 4.37793 12.5907C4.37793 12.3831 4.44414 12.204 4.57954 12.0606L4.58151 12.0586C4.72453 11.9155 4.90437 11.8447 5.11367 11.8447H8.79482C9.00363 11.8447 9.18092 11.9153 9.3177 12.0596C9.46006 12.2024 9.53057 12.3819 9.53057 12.5907C9.53057 12.7887 9.45812 12.9609 9.31671 13.1024C9.17936 13.2397 9.00235 13.306 8.79482 13.306H5.11367C4.90609 13.306 4.72695 13.2397 4.58358 13.1043L4.57954 13.1003ZM1.09476 0.851519C1.65317 0.285946 2.47955 0.0117188 3.55508 0.0117188H14.4447C15.52 0.0117188 16.3433 0.28583 16.895 0.851748C17.4529 1.41698 17.7234 2.24966 17.7234 3.33145V18.8866C17.7234 19.975 17.4531 20.8082 16.8945 21.3668C16.3427 21.9256 15.5196 22.1961 14.4447 22.1961H3.55508C2.47988 22.1961 1.65367 21.9255 1.09521 21.367C0.543652 20.8083 0.276367 19.9747 0.276367 18.8866V3.33145C0.276367 2.24984 0.543796 1.41679 1.09476 0.851519ZM15.5624 20.0351C15.2958 20.3085 14.8959 20.4452 14.3627 20.4452H3.63711C3.10391 20.4452 2.70059 20.3085 2.42715 20.0351L2.49875 19.9652L2.49786 19.9643L2.42715 20.0351C2.16055 19.7616 2.02725 19.3686 2.02725 18.8559V3.36221C2.02725 2.84951 2.16055 2.45645 2.42715 2.18301C2.70059 1.90273 3.10391 1.7626 3.63711 1.7626H14.3627C14.8959 1.7626 15.2958 1.90273 15.5624 2.18301C15.8358 2.45645 15.9726 2.84951 15.9726 3.36221V18.8559C15.9726 19.3686 15.8358 19.7616 15.5624 20.0351Z"
|
||||
fill="#3A3A3A"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
@@ -1,6 +1,6 @@
|
||||
<svg viewBox="0 0 12 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3.40918 4.70117C3.28613 4.70117 3.18359 4.66016 3.10156 4.57812C3.02409 4.49609 2.98535 4.39583 2.98535 4.27734C2.98535 4.15885 3.02409 4.06087 3.10156 3.9834C3.18359 3.90137 3.28613 3.86035 3.40918 3.86035H8.59766C8.71615 3.86035 8.81413 3.90137 8.8916 3.9834C8.97363 4.06087 9.01465 4.15885 9.01465 4.27734C9.01465 4.39583 8.97363 4.49609 8.8916 4.57812C8.81413 4.66016 8.71615 4.70117 8.59766 4.70117H3.40918ZM3.40918 7.08691C3.28613 7.08691 3.18359 7.0459 3.10156 6.96387C3.02409 6.88184 2.98535 6.78158 2.98535 6.66309C2.98535 6.5446 3.02409 6.44661 3.10156 6.36914C3.18359 6.28711 3.28613 6.24609 3.40918 6.24609H8.59766C8.71615 6.24609 8.81413 6.28711 8.8916 6.36914C8.97363 6.44661 9.01465 6.5446 9.01465 6.66309C9.01465 6.78158 8.97363 6.88184 8.8916 6.96387C8.81413 7.0459 8.71615 7.08691 8.59766 7.08691H3.40918ZM3.40918 9.47266C3.28613 9.47266 3.18359 9.43392 3.10156 9.35645C3.02409 9.27441 2.98535 9.17643 2.98535 9.0625C2.98535 8.93945 3.02409 8.83691 3.10156 8.75488C3.18359 8.67285 3.28613 8.63184 3.40918 8.63184H5.86328C5.98633 8.63184 6.08659 8.67285 6.16406 8.75488C6.24609 8.83691 6.28711 8.93945 6.28711 9.0625C6.28711 9.17643 6.24609 9.27441 6.16406 9.35645C6.08659 9.43392 5.98633 9.47266 5.86328 9.47266H3.40918ZM0.250977 13.2598V2.88965C0.250977 2.17871 0.426432 1.64323 0.777344 1.2832C1.13281 0.923177 1.66374 0.743164 2.37012 0.743164H9.62988C10.3363 0.743164 10.8649 0.923177 11.2158 1.2832C11.5713 1.64323 11.749 2.17871 11.749 2.88965V13.2598C11.749 13.9753 11.5713 14.5107 11.2158 14.8662C10.8649 15.2217 10.3363 15.3994 9.62988 15.3994H2.37012C1.66374 15.3994 1.13281 15.2217 0.777344 14.8662C0.426432 14.5107 0.250977 13.9753 0.250977 13.2598ZM1.35156 13.2393C1.35156 13.5811 1.44043 13.8431 1.61816 14.0254C1.80046 14.2077 2.06934 14.2988 2.4248 14.2988H9.5752C9.93066 14.2988 10.1973 14.2077 10.375 14.0254C10.5573 13.8431 10.6484 13.5811 10.6484 13.2393V2.91016C10.6484 2.56836 10.5573 2.30632 10.375 2.12402C10.1973 1.93717 9.93066 1.84375 9.5752 1.84375H2.4248C2.06934 1.84375 1.80046 1.93717 1.61816 2.12402C1.44043 2.30632 1.35156 2.56836 1.35156 2.91016V13.2393Z"
|
||||
fill="#8585F6"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
+39
-39
@@ -1,6 +1,7 @@
|
||||
import { codeBlock } from '@blocknote/code-block';
|
||||
import { codeBlockOptions } from '@blocknote/code-block';
|
||||
import {
|
||||
BlockNoteSchema,
|
||||
createCodeBlockSpec,
|
||||
defaultBlockSpecs,
|
||||
defaultInlineContentSpecs,
|
||||
withPageBreak,
|
||||
@@ -11,12 +12,12 @@ import { BlockNoteView } from '@blocknote/mantine';
|
||||
import '@blocknote/mantine/style.css';
|
||||
import { useCreateBlockNote } from '@blocknote/react';
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { Box, TextErrors } from '@/components';
|
||||
import { Doc, useIsCollaborativeEditable } from '@/docs/doc-management';
|
||||
import { Doc, useProviderStore } from '@/docs/doc-management';
|
||||
import { useAuth } from '@/features/auth';
|
||||
|
||||
import {
|
||||
@@ -36,7 +37,6 @@ import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
|
||||
import {
|
||||
AccessibleImageBlock,
|
||||
CalloutBlock,
|
||||
DividerBlock,
|
||||
PdfBlock,
|
||||
UploadLoaderBlock,
|
||||
} from './custom-blocks';
|
||||
@@ -53,11 +53,11 @@ const baseBlockNoteSchema = withPageBreak(
|
||||
BlockNoteSchema.create({
|
||||
blockSpecs: {
|
||||
...defaultBlockSpecs,
|
||||
callout: CalloutBlock,
|
||||
divider: DividerBlock,
|
||||
image: AccessibleImageBlock,
|
||||
pdf: PdfBlock,
|
||||
uploadLoader: UploadLoaderBlock,
|
||||
callout: CalloutBlock(),
|
||||
codeBlock: createCodeBlockSpec(codeBlockOptions),
|
||||
image: AccessibleImageBlock(),
|
||||
pdf: PdfBlock(),
|
||||
uploadLoader: UploadLoaderBlock(),
|
||||
},
|
||||
inlineContentSpecs: {
|
||||
...defaultInlineContentSpecs,
|
||||
@@ -79,28 +79,22 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
const { user } = useAuth();
|
||||
const { setEditor } = useEditorStore();
|
||||
const { t } = useTranslation();
|
||||
const { isSynced: isConnectedToCollabServer } = useProviderStore();
|
||||
const refEditorContainer = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
|
||||
const isConnectedToCollabServer = provider.isSynced;
|
||||
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
|
||||
const isDeletedDoc = !!doc.deleted_at;
|
||||
|
||||
useSaveDoc(doc.id, provider.document, !readOnly, isConnectedToCollabServer);
|
||||
useSaveDoc(doc.id, provider.document, isConnectedToCollabServer);
|
||||
const { i18n } = useTranslation();
|
||||
const lang = i18n.resolvedLanguage;
|
||||
|
||||
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
|
||||
|
||||
const collabName = readOnly
|
||||
? 'Reader'
|
||||
: user?.full_name || user?.email || t('Anonymous');
|
||||
const collabName = user?.full_name || user?.email || t('Anonymous');
|
||||
const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity';
|
||||
|
||||
const editor: DocsBlockNoteEditor = useCreateBlockNote(
|
||||
{
|
||||
codeBlock,
|
||||
collaboration: {
|
||||
provider,
|
||||
provider: provider,
|
||||
fragment: provider.document.getXmlFragment('document-store'),
|
||||
user: {
|
||||
name: collabName,
|
||||
@@ -114,10 +108,6 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
renderCursor: (user: { color: string; name: string }) => {
|
||||
const cursorElement = document.createElement('span');
|
||||
|
||||
if (user.name === 'Reader') {
|
||||
return cursorElement;
|
||||
}
|
||||
|
||||
cursorElement.classList.add('collaboration-cursor-custom__base');
|
||||
const caretElement = document.createElement('span');
|
||||
caretElement.classList.add('collaboration-cursor-custom__caret');
|
||||
@@ -166,7 +156,9 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
);
|
||||
|
||||
useHeadings(editor);
|
||||
useShortcuts(editor);
|
||||
|
||||
useShortcuts(editor, refEditorContainer.current);
|
||||
|
||||
useUploadStatus(editor);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -178,12 +170,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
}, [setEditor, editor]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
$padding={{ top: 'md' }}
|
||||
$background="white"
|
||||
$css={cssEditor(readOnly, isDeletedDoc)}
|
||||
className="--docs--editor-container"
|
||||
>
|
||||
<Box ref={refEditorContainer} $css={cssEditor}>
|
||||
{errorAttachment && (
|
||||
<Box $margin={{ bottom: 'big', top: 'none', horizontal: 'large' }}>
|
||||
<TextErrors
|
||||
@@ -198,7 +185,6 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
editor={editor}
|
||||
formattingToolbar={false}
|
||||
slashMenu={false}
|
||||
editable={!readOnly}
|
||||
theme="light"
|
||||
>
|
||||
<BlockNoteSuggestionMenu />
|
||||
@@ -208,14 +194,12 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
interface BlockNoteEditorVersionProps {
|
||||
interface BlockNoteReaderProps {
|
||||
initialContent: Y.XmlFragment;
|
||||
}
|
||||
|
||||
export const BlockNoteEditorVersion = ({
|
||||
initialContent,
|
||||
}: BlockNoteEditorVersionProps) => {
|
||||
const readOnly = true;
|
||||
export const BlockNoteReader = ({ initialContent }: BlockNoteReaderProps) => {
|
||||
const { setEditor } = useEditorStore();
|
||||
const editor = useCreateBlockNote(
|
||||
{
|
||||
collaboration: {
|
||||
@@ -231,9 +215,25 @@ export const BlockNoteEditorVersion = ({
|
||||
[initialContent],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setEditor(editor);
|
||||
|
||||
return () => {
|
||||
setEditor(undefined);
|
||||
};
|
||||
}, [setEditor, editor]);
|
||||
|
||||
useHeadings(editor);
|
||||
|
||||
return (
|
||||
<Box $css={cssEditor(readOnly, true)} className="--docs--editor-container">
|
||||
<BlockNoteView editor={editor} editable={!readOnly} theme="light" />
|
||||
<Box $css={cssEditor}>
|
||||
<BlockNoteView
|
||||
editor={editor}
|
||||
editable={false}
|
||||
theme="light"
|
||||
formattingToolbar={false}
|
||||
slashMenu={false}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
+18
-18
@@ -1,5 +1,6 @@
|
||||
import { combineByGroup, filterSuggestionItems } from '@blocknote/core';
|
||||
import {
|
||||
DefaultReactSuggestionItem,
|
||||
SuggestionMenuController,
|
||||
getDefaultReactSlashMenuItems,
|
||||
getPageBreakReactSlashMenuItems,
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
|
||||
import {
|
||||
getCalloutReactSlashMenuItems,
|
||||
getDividerReactSlashMenuItems,
|
||||
getPdfReactSlashMenuItems,
|
||||
} from './custom-blocks';
|
||||
import { useGetInterlinkingMenuItems } from './custom-inline-content';
|
||||
@@ -42,29 +42,29 @@ export const BlockNoteSuggestionMenu = () => {
|
||||
const getSlashMenuItems = useMemo(() => {
|
||||
// We insert it after the "Code Block" item to have the interlinking block displayed after the basic blocks
|
||||
const defaultMenu = getDefaultReactSlashMenuItems(editor);
|
||||
const index = defaultMenu.findIndex(
|
||||
(item) => item.aliases?.includes('code') && item.aliases?.includes('pre'),
|
||||
|
||||
const combinedMenu = combineByGroup(
|
||||
defaultMenu,
|
||||
getPageBreakReactSlashMenuItems(editor),
|
||||
getMultiColumnSlashMenuItems?.(editor) || [],
|
||||
getPdfReactSlashMenuItems(editor, t, fileBlocksName),
|
||||
getCalloutReactSlashMenuItems(editor, t, basicBlocksName),
|
||||
);
|
||||
|
||||
const index = combinedMenu.findIndex(
|
||||
(item) =>
|
||||
(item as DefaultReactSuggestionItem & { key: string })?.key ===
|
||||
'callout',
|
||||
);
|
||||
|
||||
const newSlashMenuItems = [
|
||||
...defaultMenu.slice(0, index + 1),
|
||||
...combinedMenu.slice(0, index + 1),
|
||||
...getInterlinkingMenuItems(editor, t),
|
||||
...defaultMenu.slice(index + 1),
|
||||
...combinedMenu.slice(index + 1),
|
||||
];
|
||||
|
||||
return async (query: string) =>
|
||||
Promise.resolve(
|
||||
filterSuggestionItems(
|
||||
combineByGroup(
|
||||
newSlashMenuItems,
|
||||
getCalloutReactSlashMenuItems(editor, t, basicBlocksName),
|
||||
getMultiColumnSlashMenuItems?.(editor) || [],
|
||||
getPageBreakReactSlashMenuItems(editor),
|
||||
getDividerReactSlashMenuItems(editor, t, basicBlocksName),
|
||||
getPdfReactSlashMenuItems(editor, t, fileBlocksName),
|
||||
),
|
||||
query,
|
||||
),
|
||||
);
|
||||
Promise.resolve(filterSuggestionItems(newSlashMenuItems, query));
|
||||
}, [basicBlocksName, editor, getInterlinkingMenuItems, t, fileBlocksName]);
|
||||
|
||||
return (
|
||||
|
||||
+16
-14
@@ -1,9 +1,16 @@
|
||||
/**
|
||||
* We added some custom logic to the original Blocknote FileDownloadButton
|
||||
* component to handle our file download use case.
|
||||
*
|
||||
* Original source:
|
||||
* https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDownloadButton.tsx
|
||||
*/
|
||||
|
||||
import {
|
||||
BlockSchema,
|
||||
InlineContentSchema,
|
||||
StyleSchema,
|
||||
checkBlockIsFileBlock,
|
||||
checkBlockIsFileBlockWithPlaceholder,
|
||||
blockHasType,
|
||||
} from '@blocknote/core';
|
||||
import {
|
||||
useBlockNoteEditor,
|
||||
@@ -41,7 +48,9 @@ export const FileDownloadButton = ({
|
||||
|
||||
const block = selectedBlocks[0];
|
||||
|
||||
if (checkBlockIsFileBlock(block, editor)) {
|
||||
if (
|
||||
blockHasType(block, editor, block.type, { url: 'string', name: 'string' })
|
||||
) {
|
||||
return block;
|
||||
}
|
||||
|
||||
@@ -53,6 +62,7 @@ export const FileDownloadButton = ({
|
||||
editor.focus();
|
||||
|
||||
const url = fileBlock.props.url as string;
|
||||
const name = fileBlock.props.name as string | undefined;
|
||||
|
||||
/**
|
||||
* If not hosted on our domain, means not a file uploaded by the user,
|
||||
@@ -76,16 +86,12 @@ export const FileDownloadButton = ({
|
||||
|
||||
if (!url.includes('-unsafe')) {
|
||||
const blob = (await exportResolveFileUrl(url)) as Blob;
|
||||
downloadFile(
|
||||
blob,
|
||||
fileBlock.props.name || url.split('/').pop() || 'file',
|
||||
);
|
||||
downloadFile(blob, name || url.split('/').pop() || 'file');
|
||||
} else {
|
||||
const onConfirm = async () => {
|
||||
const blob = (await exportResolveFileUrl(url)) as Blob;
|
||||
|
||||
const baseName =
|
||||
fileBlock.props.name || url.split('/').pop() || 'file';
|
||||
const baseName = name || url.split('/').pop() || 'file';
|
||||
|
||||
const regFindLastDot = /(\.[^/.]+)$/;
|
||||
const unsafeName = baseName.includes('.')
|
||||
@@ -100,11 +106,7 @@ export const FileDownloadButton = ({
|
||||
}
|
||||
}, [editor, fileBlock, open]);
|
||||
|
||||
if (
|
||||
!fileBlock ||
|
||||
checkBlockIsFileBlockWithPlaceholder(fileBlock, editor) ||
|
||||
!Components
|
||||
) {
|
||||
if (!fileBlock || fileBlock.props.url === '' || !Components) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ function isBlock(block: Block): block is Block {
|
||||
);
|
||||
}
|
||||
|
||||
const recursiveContent = (content: Block[], base: string = '') => {
|
||||
const recursiveContent = (content: Block[], base = '') => {
|
||||
let fullContent = base;
|
||||
for (const innerContent of content) {
|
||||
if (innerContent.type === 'text') {
|
||||
|
||||
@@ -1,49 +1,37 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { Box, Text, TextErrors } from '@/components';
|
||||
import { DocHeader, DocVersionHeader } from '@/docs/doc-header/';
|
||||
import { Box, Loading } from '@/components';
|
||||
import { DocHeader } from '@/docs/doc-header/';
|
||||
import {
|
||||
Doc,
|
||||
base64ToBlocknoteXmlFragment,
|
||||
useIsCollaborativeEditable,
|
||||
useProviderStore,
|
||||
} from '@/docs/doc-management';
|
||||
import { TableContent } from '@/docs/doc-table-content/';
|
||||
import { Versions, useDocVersion } from '@/docs/doc-versioning/';
|
||||
import { useSkeletonStore } from '@/features/skeletons';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { BlockNoteEditor, BlockNoteEditorVersion } from './BlockNoteEditor';
|
||||
import { BlockNoteEditor, BlockNoteReader } from './BlockNoteEditor';
|
||||
|
||||
interface DocEditorProps {
|
||||
doc: Doc;
|
||||
versionId?: Versions['version_id'];
|
||||
interface DocEditorContainerProps {
|
||||
docHeader: React.ReactNode;
|
||||
docEditor: React.ReactNode;
|
||||
isDeletedDoc: boolean;
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
||||
export const DocEditorContainer = ({
|
||||
docHeader,
|
||||
docEditor,
|
||||
isDeletedDoc,
|
||||
readOnly,
|
||||
}: DocEditorContainerProps) => {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const isVersion = !!versionId && typeof versionId === 'string';
|
||||
const { provider } = useProviderStore();
|
||||
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDesktop && !isVersion && (
|
||||
<Box
|
||||
$position="absolute"
|
||||
$css={css`
|
||||
top: 72px;
|
||||
right: 20px;
|
||||
`}
|
||||
>
|
||||
<TableContent />
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
$maxWidth="868px"
|
||||
$width="100%"
|
||||
@@ -54,7 +42,7 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
||||
$padding={{ horizontal: isDesktop ? '54px' : 'base' }}
|
||||
className="--docs--doc-editor-header"
|
||||
>
|
||||
{isVersion ? <DocVersionHeader /> : <DocHeader doc={doc} />}
|
||||
{docHeader}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
@@ -65,11 +53,17 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
||||
className="--docs--doc-editor-content"
|
||||
>
|
||||
<Box $css="flex:1;" $position="relative" $width="100%">
|
||||
{isVersion ? (
|
||||
<DocVersionEditor docId={doc.id} versionId={versionId} />
|
||||
) : (
|
||||
<BlockNoteEditor doc={doc} provider={provider} />
|
||||
)}
|
||||
<Box
|
||||
$padding={{ top: 'md', bottom: '2rem' }}
|
||||
$background="white"
|
||||
className={clsx('--docs--editor-container', {
|
||||
'--docs--doc-readonly': readOnly,
|
||||
'--docs--doc-deleted': isDeletedDoc,
|
||||
})}
|
||||
$height="100%"
|
||||
>
|
||||
{docEditor}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -77,69 +71,59 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
interface DocVersionEditorProps {
|
||||
docId: Doc['id'];
|
||||
versionId: Versions['version_id'];
|
||||
interface DocEditorProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const DocVersionEditor = ({
|
||||
docId,
|
||||
versionId,
|
||||
}: DocVersionEditorProps) => {
|
||||
const {
|
||||
data: version,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useDocVersion({
|
||||
docId,
|
||||
versionId,
|
||||
});
|
||||
|
||||
const { replace } = useRouter();
|
||||
const [initialContent, setInitialContent] = useState<Y.XmlFragment>();
|
||||
export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { provider, isReady } = useProviderStore();
|
||||
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
|
||||
const isDeletedDoc = !!doc.deleted_at;
|
||||
const readOnly =
|
||||
!doc.abilities.partial_update || !isEditable || isLoading || isDeletedDoc;
|
||||
const { setIsSkeletonVisible } = useSkeletonStore();
|
||||
const isProviderReady = isReady && provider;
|
||||
|
||||
useEffect(() => {
|
||||
if (!version?.content) {
|
||||
return;
|
||||
if (isProviderReady) {
|
||||
setIsSkeletonVisible(false);
|
||||
}
|
||||
}, [isProviderReady, setIsSkeletonVisible]);
|
||||
|
||||
setInitialContent(base64ToBlocknoteXmlFragment(version.content));
|
||||
}, [version?.content]);
|
||||
|
||||
if (isError && error) {
|
||||
if (error.status === 404) {
|
||||
void replace(`/404`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box $margin="large" className="--docs--doc-version-editor-error">
|
||||
<TextErrors
|
||||
causes={error.cause}
|
||||
icon={
|
||||
error.status === 502 ? (
|
||||
<Text
|
||||
className="material-icons"
|
||||
$theme="danger"
|
||||
aria-hidden={true}
|
||||
>
|
||||
wifi_off
|
||||
</Text>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
if (!isProviderReady) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (isLoading || !version || !initialContent) {
|
||||
return (
|
||||
<Box $align="center" $justify="center" $height="100%">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return <BlockNoteEditorVersion initialContent={initialContent} />;
|
||||
return (
|
||||
<>
|
||||
{isDesktop && (
|
||||
<Box
|
||||
$position="absolute"
|
||||
$css={css`
|
||||
top: 72px;
|
||||
right: 20px;
|
||||
`}
|
||||
>
|
||||
<TableContent />
|
||||
</Box>
|
||||
)}
|
||||
<DocEditorContainer
|
||||
docHeader={<DocHeader doc={doc} />}
|
||||
docEditor={
|
||||
readOnly ? (
|
||||
<BlockNoteReader
|
||||
initialContent={provider.document.getXmlFragment(
|
||||
'document-store',
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<BlockNoteEditor doc={doc} provider={provider} />
|
||||
)
|
||||
}
|
||||
isDeletedDoc={isDeletedDoc}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,16 +9,18 @@ interface EmojiPickerProps {
|
||||
emojiData: EmojiMartData;
|
||||
onClickOutside: () => void;
|
||||
onEmojiSelect: ({ native }: { native: string }) => void;
|
||||
withOverlay?: boolean;
|
||||
}
|
||||
|
||||
export const EmojiPicker = ({
|
||||
emojiData,
|
||||
onClickOutside,
|
||||
onEmojiSelect,
|
||||
withOverlay = false,
|
||||
}: EmojiPickerProps) => {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
const pickerContent = (
|
||||
<Box $position="absolute" $zIndex={1000} $margin="2rem 0 0 0">
|
||||
<Picker
|
||||
data={emojiData}
|
||||
@@ -30,4 +32,27 @@ export const EmojiPicker = ({
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
if (withOverlay) {
|
||||
return (
|
||||
<>
|
||||
{/* Overlay transparent pour fermer en cliquant à l'extérieur */}
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
zIndex: 999,
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
onClick={onClickOutside}
|
||||
/>
|
||||
{pickerContent}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return pickerContent;
|
||||
};
|
||||
|
||||
+74
-51
@@ -1,43 +1,62 @@
|
||||
/**
|
||||
* AccessibleImageBlock.tsx
|
||||
*
|
||||
* This file defines a custom BlockNote block specification for an accessible image block.
|
||||
* It extends the default image block to ensure compliance with accessibility standards,
|
||||
* specifically RGAA 1.9.1, by using <figure> and <figcaption> elements when a caption is provided.
|
||||
*
|
||||
* The accessible image block ensures that:
|
||||
* - Images with captions are wrapped in <figure> and <figcaption> elements.
|
||||
* - The <img> element has an appropriate alt attribute based on the caption.
|
||||
* - Accessibility attributes such as role and aria-label are added for better screen reader support.
|
||||
* - Images without captions have alt="" and are marked as decorative with aria-hidden="true".
|
||||
*
|
||||
* This implementation leverages BlockNote's existing image block functionality while enhancing it for accessibility.
|
||||
* https://github.com/TypeCellOS/BlockNote/blob/main/packages/core/src/blocks/Image/block.ts
|
||||
*/
|
||||
|
||||
import {
|
||||
BlockFromConfig,
|
||||
BlockNoteEditor,
|
||||
BlockSchemaWithBlock,
|
||||
ImageOptions,
|
||||
InlineContentSchema,
|
||||
InlineContentSchemaFromSpecs,
|
||||
StyleSchema,
|
||||
createBlockSpec,
|
||||
imageBlockConfig,
|
||||
createImageBlockConfig,
|
||||
defaultInlineContentSpecs,
|
||||
imageParse,
|
||||
imageRender,
|
||||
imageToExternalHTML,
|
||||
} from '@blocknote/core';
|
||||
import { t } from 'i18next';
|
||||
|
||||
type ImageBlockConfig = typeof imageBlockConfig;
|
||||
type CreateImageBlockConfig = ReturnType<typeof createImageBlockConfig>;
|
||||
|
||||
export const accessibleImageRender = (
|
||||
block: BlockFromConfig<ImageBlockConfig, InlineContentSchema, StyleSchema>,
|
||||
editor: BlockNoteEditor<
|
||||
BlockSchemaWithBlock<ImageBlockConfig['type'], ImageBlockConfig>,
|
||||
InlineContentSchema,
|
||||
StyleSchema
|
||||
>,
|
||||
) => {
|
||||
const imageRenderComputed = imageRender(block, editor);
|
||||
const dom = imageRenderComputed.dom;
|
||||
const imgSelector = dom.querySelector('img');
|
||||
|
||||
const withCaption =
|
||||
block.props.caption && dom.querySelector('.bn-file-caption');
|
||||
|
||||
const accessibleImageWithCaption = () => {
|
||||
imgSelector?.setAttribute('alt', block.props.caption);
|
||||
imgSelector?.removeAttribute('aria-hidden');
|
||||
imgSelector?.setAttribute('tabindex', '0');
|
||||
export const accessibleImageRender =
|
||||
(config: ImageOptions) =>
|
||||
(
|
||||
block: BlockFromConfig<
|
||||
CreateImageBlockConfig,
|
||||
InlineContentSchema,
|
||||
StyleSchema
|
||||
>,
|
||||
editor: BlockNoteEditor<
|
||||
Record<'image', CreateImageBlockConfig>,
|
||||
InlineContentSchemaFromSpecs<typeof defaultInlineContentSpecs>,
|
||||
StyleSchema
|
||||
>,
|
||||
) => {
|
||||
const imageRenderComputed = imageRender(config);
|
||||
const dom = imageRenderComputed(block, editor).dom;
|
||||
const imgSelector = dom.querySelector('img');
|
||||
|
||||
// Fix RGAA 1.9.1: Convert to figure/figcaption structure if caption exists
|
||||
const captionElement = dom.querySelector('.bn-file-caption');
|
||||
const accessibleImageWithCaption = () => {
|
||||
imgSelector?.setAttribute('alt', block.props.caption);
|
||||
imgSelector?.removeAttribute('aria-hidden');
|
||||
imgSelector?.setAttribute('tabindex', '0');
|
||||
|
||||
if (captionElement) {
|
||||
const figureElement = document.createElement('figure');
|
||||
|
||||
// Copy all attributes from the original div
|
||||
@@ -76,32 +95,36 @@ export const accessibleImageRender = (
|
||||
...imageRenderComputed,
|
||||
dom: figureElement,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const accessibleImage = () => {
|
||||
imgSelector?.setAttribute('alt', '');
|
||||
imgSelector?.setAttribute('role', 'presentation');
|
||||
imgSelector?.setAttribute('aria-hidden', 'true');
|
||||
imgSelector?.setAttribute('tabindex', '-1');
|
||||
|
||||
return {
|
||||
...imageRenderComputed,
|
||||
dom,
|
||||
};
|
||||
};
|
||||
|
||||
const withCaption =
|
||||
block.props.caption && dom.querySelector('.bn-file-caption');
|
||||
|
||||
// Set accessibility attributes for the image
|
||||
return withCaption ? accessibleImageWithCaption() : accessibleImage();
|
||||
};
|
||||
|
||||
const accessibleImage = () => {
|
||||
imgSelector?.setAttribute('alt', '');
|
||||
imgSelector?.setAttribute('role', 'presentation');
|
||||
imgSelector?.setAttribute('aria-hidden', 'true');
|
||||
imgSelector?.setAttribute('tabindex', '-1');
|
||||
};
|
||||
|
||||
// Set accessibility attributes for the image
|
||||
const result = withCaption ? accessibleImageWithCaption() : accessibleImage();
|
||||
|
||||
// Return the result if accessibleImageWithCaption created a figure, otherwise return original
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
...imageRenderComputed,
|
||||
dom,
|
||||
};
|
||||
};
|
||||
|
||||
export const AccessibleImageBlock = createBlockSpec(imageBlockConfig, {
|
||||
render: accessibleImageRender,
|
||||
parse: imageParse,
|
||||
toExternalHTML: imageToExternalHTML,
|
||||
});
|
||||
export const AccessibleImageBlock = createBlockSpec(
|
||||
createImageBlockConfig,
|
||||
(config) => ({
|
||||
meta: {
|
||||
fileBlockAccept: ['image/*'],
|
||||
},
|
||||
render: accessibleImageRender(config),
|
||||
parse: imageParse(config),
|
||||
toExternalHTML: imageToExternalHTML(config),
|
||||
runsBefore: ['file'],
|
||||
}),
|
||||
);
|
||||
|
||||
+126
-77
@@ -1,9 +1,16 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { defaultProps, insertOrUpdateBlock } from '@blocknote/core';
|
||||
import {
|
||||
BlockConfig,
|
||||
BlockNoDefaults,
|
||||
BlockNoteEditor,
|
||||
InlineContentSchema,
|
||||
StyleSchema,
|
||||
defaultProps,
|
||||
insertOrUpdateBlock,
|
||||
} from '@blocknote/core';
|
||||
import { BlockTypeSelectItem, createReactBlockSpec } from '@blocknote/react';
|
||||
import { TFunction } from 'i18next';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
import { createGlobalStyle, css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, Icon } from '@/components';
|
||||
|
||||
@@ -12,90 +19,131 @@ import { EmojiPicker } from '../EmojiPicker';
|
||||
|
||||
import emojidata from './initEmojiCallout';
|
||||
|
||||
const CalloutBlockStyle = createGlobalStyle`
|
||||
.bn-block-content[data-content-type="callout"][data-background-color] {
|
||||
padding: var(--c--theme--spacings--3xs) var(--c--theme--spacings--3xs);
|
||||
border-radius: var(--c--theme--spacings--3xs);
|
||||
}
|
||||
`;
|
||||
|
||||
type CreateCalloutBlockConfig = BlockConfig<
|
||||
'callout',
|
||||
{
|
||||
textAlignment: typeof defaultProps.textAlignment;
|
||||
backgroundColor: typeof defaultProps.backgroundColor;
|
||||
emoji: { default: '💡' };
|
||||
},
|
||||
'inline'
|
||||
>;
|
||||
|
||||
interface CalloutComponentProps {
|
||||
block: BlockNoDefaults<
|
||||
Record<'callout', CreateCalloutBlockConfig>,
|
||||
InlineContentSchema,
|
||||
StyleSchema
|
||||
>;
|
||||
editor: BlockNoteEditor<
|
||||
Record<'callout', CreateCalloutBlockConfig>,
|
||||
InlineContentSchema,
|
||||
StyleSchema
|
||||
>;
|
||||
contentRef: (node: HTMLElement | null) => void;
|
||||
}
|
||||
|
||||
const CalloutComponent = ({
|
||||
block,
|
||||
editor,
|
||||
contentRef,
|
||||
}: CalloutComponentProps) => {
|
||||
const [openEmojiPicker, setOpenEmojiPicker] = useState(false);
|
||||
const isEditable = editor.isEditable;
|
||||
|
||||
const toggleEmojiPicker = (e: React.MouseEvent) => {
|
||||
if (!isEditable) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setOpenEmojiPicker(!openEmojiPicker);
|
||||
};
|
||||
|
||||
const onClickOutside = () => setOpenEmojiPicker(false);
|
||||
|
||||
const onEmojiSelect = ({ native }: { native: string }) => {
|
||||
editor.updateBlock(block, { props: { emoji: native } });
|
||||
setOpenEmojiPicker(false);
|
||||
};
|
||||
|
||||
// Temporary: sets a yellow background color to a callout block when added by
|
||||
// the user, while keeping the colors menu on the drag handler usable for
|
||||
// this custom block.
|
||||
useEffect(() => {
|
||||
if (!block.content.length && block.props.backgroundColor === 'default') {
|
||||
// Delay the update to avoid interfering with the block insertion process
|
||||
setTimeout(() => {
|
||||
editor.updateBlock(block, { props: { backgroundColor: 'yellow' } });
|
||||
}, 0);
|
||||
}
|
||||
}, [block, editor]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
$padding="1rem"
|
||||
$gap="0.625rem"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$css={css`
|
||||
flex-grow: 1;
|
||||
`}
|
||||
>
|
||||
<CalloutBlockStyle />
|
||||
<BoxButton
|
||||
contentEditable={false}
|
||||
onClick={toggleEmojiPicker}
|
||||
$css={css`
|
||||
font-size: 1.125rem;
|
||||
cursor: ${isEditable ? 'pointer' : 'default'};
|
||||
${isEditable &&
|
||||
`
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`}
|
||||
`}
|
||||
$align="center"
|
||||
$width="28px"
|
||||
$radius="4px"
|
||||
>
|
||||
{block.props.emoji}
|
||||
</BoxButton>
|
||||
|
||||
{openEmojiPicker && (
|
||||
<EmojiPicker
|
||||
emojiData={emojidata}
|
||||
onClickOutside={onClickOutside}
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
withOverlay={true}
|
||||
/>
|
||||
)}
|
||||
<Box as="p" className="inline-content" ref={contentRef} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const CalloutBlock = createReactBlockSpec(
|
||||
{
|
||||
type: 'callout',
|
||||
propSchema: {
|
||||
textAlignment: defaultProps.textAlignment,
|
||||
backgroundColor: defaultProps.backgroundColor,
|
||||
backgroundColor: { default: 'default' as const },
|
||||
emoji: { default: '💡' },
|
||||
},
|
||||
content: 'inline',
|
||||
},
|
||||
{
|
||||
render: ({ block, editor, contentRef }) => {
|
||||
const [openEmojiPicker, setOpenEmojiPicker] = useState(false);
|
||||
const isEditable = editor.isEditable;
|
||||
|
||||
const toggleEmojiPicker = (e: React.MouseEvent) => {
|
||||
if (!isEditable) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setOpenEmojiPicker(!openEmojiPicker);
|
||||
};
|
||||
|
||||
const onClickOutside = () => setOpenEmojiPicker(false);
|
||||
|
||||
const onEmojiSelect = ({ native }: { native: string }) => {
|
||||
editor.updateBlock(block, { props: { emoji: native } });
|
||||
setOpenEmojiPicker(false);
|
||||
};
|
||||
|
||||
// Temporary: sets a yellow background color to a callout block when added by
|
||||
// the user, while keeping the colors menu on the drag handler usable for
|
||||
// this custom block.
|
||||
useEffect(() => {
|
||||
if (
|
||||
!block.content.length &&
|
||||
block.props.backgroundColor === 'default'
|
||||
) {
|
||||
editor.updateBlock(block, { props: { backgroundColor: 'yellow' } });
|
||||
}
|
||||
}, [block, editor]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
$padding="1rem"
|
||||
$gap="0.625rem"
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
flexDirection: 'row',
|
||||
}}
|
||||
>
|
||||
<BoxButton
|
||||
contentEditable={false}
|
||||
onClick={toggleEmojiPicker}
|
||||
$css={css`
|
||||
font-size: 1.125rem;
|
||||
cursor: ${isEditable ? 'pointer' : 'default'};
|
||||
${isEditable &&
|
||||
`
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`}
|
||||
`}
|
||||
$align="center"
|
||||
$height="28px"
|
||||
$width="28px"
|
||||
$radius="4px"
|
||||
>
|
||||
{block.props.emoji}
|
||||
</BoxButton>
|
||||
|
||||
{openEmojiPicker && (
|
||||
<EmojiPicker
|
||||
emojiData={emojidata}
|
||||
onClickOutside={onClickOutside}
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
/>
|
||||
)}
|
||||
<Box as="p" className="inline-content" ref={contentRef} />
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
render: ({ block, editor, contentRef }) => (
|
||||
<CalloutComponent block={block} editor={editor} contentRef={contentRef} />
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -105,6 +153,7 @@ export const getCalloutReactSlashMenuItems = (
|
||||
group: string,
|
||||
) => [
|
||||
{
|
||||
key: 'callout',
|
||||
title: t('Callout'),
|
||||
onItemClick: () => {
|
||||
insertOrUpdateBlock(editor, {
|
||||
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import { insertOrUpdateBlock } from '@blocknote/core';
|
||||
import { createReactBlockSpec } from '@blocknote/react';
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
import { Box, Icon } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../../types';
|
||||
|
||||
export const DividerBlock = createReactBlockSpec(
|
||||
{
|
||||
type: 'divider',
|
||||
propSchema: {},
|
||||
content: 'none',
|
||||
},
|
||||
{
|
||||
render: () => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="hr"
|
||||
$width="100%"
|
||||
$background={colorsTokens['greyscale-300']}
|
||||
$margin="1rem 0"
|
||||
$css={`border: 1px solid ${colorsTokens['greyscale-300']};`}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const getDividerReactSlashMenuItems = (
|
||||
editor: DocsBlockNoteEditor,
|
||||
t: TFunction<'translation', undefined>,
|
||||
group: string,
|
||||
) => [
|
||||
{
|
||||
title: t('Divider'),
|
||||
onItemClick: () => {
|
||||
insertOrUpdateBlock(editor, {
|
||||
type: 'divider',
|
||||
});
|
||||
},
|
||||
aliases: ['divider', 'hr', 'horizontal rule', 'line', 'separator'],
|
||||
group,
|
||||
icon: <Icon iconName="remove" $size="18px" />,
|
||||
subtext: t('Add a horizontal line'),
|
||||
},
|
||||
];
|
||||
+98
-41
@@ -1,11 +1,19 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { insertOrUpdateBlock } from '@blocknote/core';
|
||||
import {
|
||||
BlockConfig,
|
||||
BlockNoDefaults,
|
||||
BlockNoteEditor,
|
||||
InlineContentSchema,
|
||||
StyleSchema,
|
||||
insertOrUpdateBlock,
|
||||
} from '@blocknote/core';
|
||||
import * as locales from '@blocknote/core/locales';
|
||||
import {
|
||||
AddFileButton,
|
||||
ResizableFileBlockWrapper,
|
||||
createReactBlockSpec,
|
||||
} from '@blocknote/react';
|
||||
import { TFunction } from 'i18next';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
@@ -20,57 +28,106 @@ const PDFBlockStyle = createGlobalStyle`
|
||||
`;
|
||||
|
||||
type FileBlockEditor = Parameters<typeof AddFileButton>[0]['editor'];
|
||||
type FileBlockBlock = Parameters<typeof AddFileButton>[0]['block'];
|
||||
|
||||
type CreatePDFBlockConfig = BlockConfig<
|
||||
'pdf',
|
||||
{
|
||||
backgroundColor: { default: 'default' };
|
||||
caption: { default: '' };
|
||||
name: { default: '' };
|
||||
previewWidth: { default: undefined; type: 'number' };
|
||||
showPreview: { default: true };
|
||||
textAlignment: { default: 'left' };
|
||||
url: { default: '' };
|
||||
},
|
||||
'none'
|
||||
>;
|
||||
|
||||
interface PdfBlockComponentProps {
|
||||
block: BlockNoDefaults<
|
||||
Record<'pdf', CreatePDFBlockConfig>,
|
||||
InlineContentSchema,
|
||||
StyleSchema
|
||||
>;
|
||||
contentRef: (node: HTMLElement | null) => void;
|
||||
editor: BlockNoteEditor<
|
||||
Record<'pdf', CreatePDFBlockConfig>,
|
||||
InlineContentSchema,
|
||||
StyleSchema
|
||||
>;
|
||||
}
|
||||
|
||||
const PdfBlockComponent = ({
|
||||
editor,
|
||||
block,
|
||||
contentRef,
|
||||
}: PdfBlockComponentProps) => {
|
||||
const pdfUrl = block.props.url;
|
||||
const { i18n, t } = useTranslation();
|
||||
const lang = i18n.resolvedLanguage;
|
||||
|
||||
useEffect(() => {
|
||||
if (lang && locales[lang as keyof typeof locales]) {
|
||||
locales[lang as keyof typeof locales].file_blocks.add_button_text['pdf'] =
|
||||
t('Add PDF');
|
||||
(
|
||||
locales[lang as keyof typeof locales].file_panel.embed
|
||||
.embed_button as Record<string, string>
|
||||
)['pdf'] = t('Add PDF');
|
||||
(
|
||||
locales[lang as keyof typeof locales].file_panel.upload
|
||||
.file_placeholder as Record<string, string>
|
||||
)['pdf'] = t('Upload PDF');
|
||||
}
|
||||
}, [lang, t]);
|
||||
|
||||
return (
|
||||
<Box ref={contentRef} className="bn-file-block-content-wrapper">
|
||||
<PDFBlockStyle />
|
||||
<ResizableFileBlockWrapper
|
||||
buttonIcon={
|
||||
<Icon iconName="upload" $size="24px" $css="line-height: normal;" />
|
||||
}
|
||||
block={block as unknown as FileBlockBlock}
|
||||
editor={editor as unknown as FileBlockEditor}
|
||||
>
|
||||
<Box
|
||||
className="bn-visual-media"
|
||||
role="presentation"
|
||||
as="embed"
|
||||
$width="100%"
|
||||
$height="450px"
|
||||
type="application/pdf"
|
||||
src={pdfUrl}
|
||||
contentEditable={false}
|
||||
draggable={false}
|
||||
onClick={() => editor.setTextCursorPosition(block)}
|
||||
/>
|
||||
</ResizableFileBlockWrapper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const PdfBlock = createReactBlockSpec(
|
||||
{
|
||||
type: 'pdf',
|
||||
content: 'none',
|
||||
propSchema: {
|
||||
name: { default: '' as const },
|
||||
url: { default: '' as const },
|
||||
backgroundColor: { default: 'default' as const },
|
||||
caption: { default: '' as const },
|
||||
showPreview: { default: true },
|
||||
name: { default: '' as const },
|
||||
previewWidth: { default: undefined, type: 'number' },
|
||||
showPreview: { default: true },
|
||||
textAlignment: { default: 'left' as const },
|
||||
url: { default: '' as const },
|
||||
},
|
||||
isFileBlock: true,
|
||||
fileBlockAccept: ['application/pdf'],
|
||||
},
|
||||
{
|
||||
render: ({ editor, block, contentRef }) => {
|
||||
const { t } = useTranslation();
|
||||
const pdfUrl = block.props.url;
|
||||
|
||||
return (
|
||||
<Box ref={contentRef} className="bn-file-block-content-wrapper">
|
||||
<PDFBlockStyle />
|
||||
<ResizableFileBlockWrapper
|
||||
buttonIcon={
|
||||
<Icon
|
||||
iconName="upload"
|
||||
$size="24px"
|
||||
$css="line-height: normal;"
|
||||
/>
|
||||
}
|
||||
block={block}
|
||||
editor={editor as unknown as FileBlockEditor}
|
||||
buttonText={t('Add PDF')}
|
||||
>
|
||||
<Box
|
||||
className="bn-visual-media"
|
||||
role="presentation"
|
||||
as="embed"
|
||||
$width="100%"
|
||||
$height="450px"
|
||||
type="application/pdf"
|
||||
src={pdfUrl}
|
||||
contentEditable={false}
|
||||
draggable={false}
|
||||
onClick={() => editor.setTextCursorPosition(block)}
|
||||
/>
|
||||
</ResizableFileBlockWrapper>
|
||||
</Box>
|
||||
);
|
||||
meta: {
|
||||
fileBlockAccept: ['application/pdf'],
|
||||
},
|
||||
render: (props) => <PdfBlockComponent {...props} />,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+127
-15
@@ -1,34 +1,146 @@
|
||||
import {
|
||||
BlockConfig,
|
||||
BlockNoDefaults,
|
||||
BlockNoteEditor,
|
||||
InlineContentSchema,
|
||||
StyleSchema,
|
||||
} from '@blocknote/core';
|
||||
import { createReactBlockSpec } from '@blocknote/react';
|
||||
import { t } from 'i18next';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
import { useMediaUrl } from '@/core';
|
||||
|
||||
import { loopCheckDocMediaStatus } from '../../api';
|
||||
import Loader from '../../assets/loader.svg';
|
||||
import Warning from '../../assets/warning.svg';
|
||||
|
||||
type UploadLoaderPropSchema = {
|
||||
readonly information: { readonly default: '' };
|
||||
readonly type: {
|
||||
readonly default: 'loading';
|
||||
readonly values: readonly ['loading', 'warning'];
|
||||
};
|
||||
readonly blockUploadName: { readonly default: '' };
|
||||
readonly blockUploadShowPreview: { readonly default: true };
|
||||
readonly blockUploadType: {
|
||||
readonly default: '';
|
||||
};
|
||||
readonly blockUploadUrl: { readonly default: '' };
|
||||
};
|
||||
|
||||
type UploadLoaderBlockConfig = BlockConfig<
|
||||
'uploadLoader',
|
||||
UploadLoaderPropSchema,
|
||||
'none'
|
||||
>;
|
||||
|
||||
type UploadLoaderBlockType = BlockNoDefaults<
|
||||
Record<'uploadLoader', UploadLoaderBlockConfig>,
|
||||
InlineContentSchema,
|
||||
StyleSchema
|
||||
>;
|
||||
|
||||
type UploadLoaderEditor = BlockNoteEditor<
|
||||
Record<'uploadLoader', UploadLoaderBlockConfig>,
|
||||
InlineContentSchema,
|
||||
StyleSchema
|
||||
>;
|
||||
|
||||
interface UploadLoaderBlockComponentProps {
|
||||
block: UploadLoaderBlockType;
|
||||
editor: UploadLoaderEditor;
|
||||
contentRef: (node: HTMLElement | null) => void;
|
||||
}
|
||||
|
||||
const UploadLoaderBlockComponent = ({
|
||||
block,
|
||||
editor,
|
||||
}: UploadLoaderBlockComponentProps) => {
|
||||
const mediaUrl = useMediaUrl();
|
||||
|
||||
useEffect(() => {
|
||||
if (!block.props.blockUploadUrl || block.props.type !== 'loading') {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = block.props.blockUploadUrl;
|
||||
|
||||
loopCheckDocMediaStatus(url)
|
||||
.then((response) => {
|
||||
// Replace the loading block with the resource block (image, audio, video, pdf ...)
|
||||
try {
|
||||
editor.replaceBlocks(
|
||||
[block.id],
|
||||
[
|
||||
{
|
||||
type: block.props.blockUploadType,
|
||||
props: {
|
||||
url: `${mediaUrl}${response.file}`,
|
||||
showPreview: block.props.blockUploadShowPreview,
|
||||
name: block.props.blockUploadName,
|
||||
caption: '',
|
||||
backgroundColor: 'default',
|
||||
textAlignment: 'left',
|
||||
},
|
||||
} as never,
|
||||
],
|
||||
);
|
||||
} catch {
|
||||
/* During collaboration, another user might have updated the block */
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error analyzing file:', error);
|
||||
|
||||
try {
|
||||
editor.updateBlock(block.id, {
|
||||
type: 'uploadLoader',
|
||||
props: {
|
||||
type: 'warning',
|
||||
information: t(
|
||||
'The antivirus has detected an anomaly in your file.',
|
||||
),
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
/* During collaboration, another user might have updated the block */
|
||||
}
|
||||
});
|
||||
}, [block, editor, mediaUrl]);
|
||||
|
||||
return (
|
||||
<Box className="bn-visual-media-wrapper" $direction="row" $gap="0.5rem">
|
||||
{block.props.type === 'warning' ? (
|
||||
<Warning />
|
||||
) : (
|
||||
<Loader style={{ animation: 'spin 1.5s linear infinite' }} />
|
||||
)}
|
||||
<Text>{block.props.information}</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const UploadLoaderBlock = createReactBlockSpec(
|
||||
{
|
||||
type: 'uploadLoader',
|
||||
propSchema: {
|
||||
information: { default: '' as const },
|
||||
information: { default: '' },
|
||||
type: {
|
||||
default: 'loading' as const,
|
||||
values: ['loading', 'warning'] as const,
|
||||
default: 'loading',
|
||||
values: ['loading', 'warning'],
|
||||
},
|
||||
blockUploadName: { default: '' },
|
||||
blockUploadShowPreview: { default: true },
|
||||
blockUploadType: {
|
||||
default: '',
|
||||
},
|
||||
blockUploadUrl: { default: '' },
|
||||
},
|
||||
content: 'none',
|
||||
},
|
||||
{
|
||||
render: ({ block }) => {
|
||||
return (
|
||||
<Box className="bn-visual-media-wrapper" $direction="row" $gap="0.5rem">
|
||||
{block.props.type === 'warning' ? (
|
||||
<Warning />
|
||||
) : (
|
||||
<Loader style={{ animation: 'spin 1.5s linear infinite' }} />
|
||||
)}
|
||||
<Text>{block.props.information}</Text>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
render: (props) => <UploadLoaderBlockComponent {...props} />,
|
||||
},
|
||||
);
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
export * from './AccessibleImageBlock';
|
||||
export * from './CalloutBlock';
|
||||
export * from './DividerBlock';
|
||||
export { default as emojidata } from './initEmojiCallout';
|
||||
export * from './PdfBlock';
|
||||
export * from './UploadLoaderBlock';
|
||||
|
||||
+36
-7
@@ -7,7 +7,7 @@ import { css } from 'styled-components';
|
||||
import { BoxButton, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import SelectedPageIcon from '@/docs/doc-editor/assets/doc-selected.svg';
|
||||
import { useDoc } from '@/docs/doc-management';
|
||||
import { getEmojiAndTitle, useDoc } from '@/docs/doc-management';
|
||||
|
||||
export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
|
||||
{
|
||||
@@ -29,6 +29,9 @@ export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
|
||||
render: ({ inlineContent, updateInlineContent }) => {
|
||||
const { data: doc } = useDoc({ id: inlineContent.props.docId });
|
||||
|
||||
/**
|
||||
* Update the content title if the referenced doc title changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (doc?.title && doc.title !== inlineContent.props.title) {
|
||||
updateInlineContent({
|
||||
@@ -39,7 +42,15 @@ export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [inlineContent.props, doc?.title, updateInlineContent]);
|
||||
|
||||
/**
|
||||
* ⚠️ When doing collaborative editing, doc?.title might be out of sync
|
||||
* causing an infinite loop of updates.
|
||||
* To prevent this, we only run this effect when doc?.title changes,
|
||||
* not when inlineContent.props.title changes.
|
||||
*/
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [doc?.title]);
|
||||
|
||||
return <LinkSelected {...inlineContent.props} />;
|
||||
},
|
||||
@@ -52,11 +63,13 @@ interface LinkSelectedProps {
|
||||
}
|
||||
const LinkSelected = ({ url, title }: LinkSelectedProps) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(title);
|
||||
const router = useRouter();
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
router.push(url);
|
||||
void router.push(url);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -64,7 +77,7 @@ const LinkSelected = ({ url, title }: LinkSelectedProps) => {
|
||||
onClick={handleClick}
|
||||
draggable="false"
|
||||
$css={css`
|
||||
display: inline;
|
||||
display: contents;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
& svg {
|
||||
@@ -76,11 +89,27 @@ const LinkSelected = ({ url, title }: LinkSelectedProps) => {
|
||||
background-color: ${colorsTokens['greyscale-100']};
|
||||
}
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
|
||||
.--docs--doc-deleted & {
|
||||
pointer-events: none;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<SelectedPageIcon width={11.5} />
|
||||
<Text $weight="500" spellCheck="false" $size="16px" $display="inline">
|
||||
{title}
|
||||
{emoji ? (
|
||||
<Text $size="16px">{emoji}</Text>
|
||||
) : (
|
||||
<SelectedPageIcon width={11.5} color={colorsTokens['primary-400']} />
|
||||
)}
|
||||
<Text
|
||||
$weight="500"
|
||||
spellCheck="false"
|
||||
$size="16px"
|
||||
$display="inline"
|
||||
$css={css`
|
||||
margin-left: 2px;
|
||||
`}
|
||||
>
|
||||
{titleWithoutEmoji}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
);
|
||||
|
||||
+2
@@ -47,6 +47,7 @@ export const getInterlinkinghMenuItems = (
|
||||
createPage: () => void,
|
||||
) => [
|
||||
{
|
||||
key: 'link-doc',
|
||||
title: t('Link a doc'),
|
||||
onItemClick: () => {
|
||||
editor.insertInlineContent([
|
||||
@@ -65,6 +66,7 @@ export const getInterlinkinghMenuItems = (
|
||||
subtext: t('Link this doc to another doc'),
|
||||
},
|
||||
{
|
||||
key: 'new-sub-doc',
|
||||
title: t('New sub-doc'),
|
||||
onItemClick: createPage,
|
||||
aliases: ['new sub-doc'],
|
||||
|
||||
+56
-32
@@ -25,6 +25,7 @@ import {
|
||||
import FoundPageIcon from '@/docs/doc-editor/assets/doc-found.svg';
|
||||
import AddPageIcon from '@/docs/doc-editor/assets/doc-plus.svg';
|
||||
import {
|
||||
getEmojiAndTitle,
|
||||
useCreateChildDocTree,
|
||||
useDocStore,
|
||||
useTrans,
|
||||
@@ -43,17 +44,19 @@ const inputStyle = css`
|
||||
`;
|
||||
|
||||
type SearchPageProps = {
|
||||
trigger: string;
|
||||
trigger: '/' | '@';
|
||||
updateInlineContent: (
|
||||
update: PartialCustomInlineContentFromConfig<
|
||||
{
|
||||
type: string;
|
||||
type: 'interlinkingSearchInline';
|
||||
propSchema: {
|
||||
disabled: {
|
||||
default: boolean;
|
||||
default: false;
|
||||
values: [true, false];
|
||||
};
|
||||
trigger: {
|
||||
default: string;
|
||||
default: '/';
|
||||
values: ['/', '@'];
|
||||
};
|
||||
};
|
||||
content: 'styled';
|
||||
@@ -234,35 +237,56 @@ export const SearchPage = ({
|
||||
|
||||
editor.focus();
|
||||
}}
|
||||
renderElement={(doc) => (
|
||||
<QuickSearchItemContent
|
||||
left={
|
||||
<Box
|
||||
$direction="row"
|
||||
$gap="0.6rem"
|
||||
$align="center"
|
||||
$padding={{ vertical: '0.5rem', horizontal: '0.2rem' }}
|
||||
$width="100%"
|
||||
>
|
||||
<FoundPageIcon />
|
||||
<Text
|
||||
$size="14px"
|
||||
$color="var(--c--theme--colors--greyscale-1000)"
|
||||
spellCheck="false"
|
||||
renderElement={(doc) => {
|
||||
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(
|
||||
doc.title || untitledDocument,
|
||||
);
|
||||
|
||||
return (
|
||||
<QuickSearchItemContent
|
||||
left={
|
||||
<Box
|
||||
$direction="row"
|
||||
$gap="0.2rem"
|
||||
$align="center"
|
||||
$padding={{ vertical: '0.5rem', horizontal: '0.2rem' }}
|
||||
$width="100%"
|
||||
>
|
||||
{doc.title}
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
right={
|
||||
<Icon
|
||||
iconName="keyboard_return"
|
||||
$variation="600"
|
||||
spellCheck="false"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Box
|
||||
$css={css`
|
||||
width: 24px;
|
||||
flex-shrink: 0;
|
||||
`}
|
||||
>
|
||||
{emoji ? (
|
||||
<Text $size="18px">{emoji}</Text>
|
||||
) : (
|
||||
<FoundPageIcon
|
||||
width="100%"
|
||||
style={{ maxHeight: '24px' }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Text
|
||||
$size="14px"
|
||||
$color="var(--c--theme--colors--greyscale-1000)"
|
||||
spellCheck="false"
|
||||
>
|
||||
{titleWithoutEmoji}
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
right={
|
||||
<Icon
|
||||
iconName="keyboard_return"
|
||||
$variation="600"
|
||||
spellCheck="false"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<QuickSearchGroup
|
||||
group={{
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './BlockNoteEditor';
|
||||
export * from './DocEditor';
|
||||
export * from './EmojiPicker';
|
||||
export * from './custom-blocks/';
|
||||
|
||||
+4
-35
@@ -43,7 +43,7 @@ describe('useSaveDoc', () => {
|
||||
|
||||
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
|
||||
|
||||
renderHook(() => useSaveDoc(docId, yDoc, true, true), {
|
||||
renderHook(() => useSaveDoc(docId, yDoc, true), {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
|
||||
@@ -62,37 +62,6 @@ describe('useSaveDoc', () => {
|
||||
addEventListenerSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not save when canSave is false', () => {
|
||||
vi.useFakeTimers();
|
||||
const yDoc = new Y.Doc();
|
||||
const docId = 'test-doc-id';
|
||||
|
||||
fetchMock.patch('http://test.jest/api/v1.0/documents/test-doc-id/', {
|
||||
body: JSON.stringify({
|
||||
id: 'test-doc-id',
|
||||
content: 'test-content',
|
||||
title: 'test-title',
|
||||
}),
|
||||
});
|
||||
|
||||
renderHook(() => useSaveDoc(docId, yDoc, false, true), {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
// Trigger a local update
|
||||
yDoc.getMap('test').set('key', 'value');
|
||||
|
||||
// Advance timers to trigger the save interval
|
||||
vi.advanceTimersByTime(61000);
|
||||
});
|
||||
|
||||
// Since canSave is false, no API call should be made
|
||||
expect(fetchMock.calls().length).toBe(0);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should save when there are local changes', async () => {
|
||||
vi.useFakeTimers();
|
||||
const yDoc = new Y.Doc();
|
||||
@@ -106,7 +75,7 @@ describe('useSaveDoc', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
renderHook(() => useSaveDoc(docId, yDoc, true, true), {
|
||||
renderHook(() => useSaveDoc(docId, yDoc, true), {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
|
||||
@@ -143,7 +112,7 @@ describe('useSaveDoc', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
renderHook(() => useSaveDoc(docId, yDoc, true, true), {
|
||||
renderHook(() => useSaveDoc(docId, yDoc, true), {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
|
||||
@@ -163,7 +132,7 @@ describe('useSaveDoc', () => {
|
||||
const docId = 'test-doc-id';
|
||||
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
|
||||
|
||||
const { unmount } = renderHook(() => useSaveDoc(docId, yDoc, true, true), {
|
||||
const { unmount } = renderHook(() => useSaveDoc(docId, yDoc, true), {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
|
||||
|
||||
@@ -9,12 +9,13 @@ export const useHeadings = (editor: DocsBlockNoteEditor) => {
|
||||
useEffect(() => {
|
||||
setHeadings(editor);
|
||||
|
||||
editor?.onChange(() => {
|
||||
const unsubscribe = editor?.onChange(() => {
|
||||
setHeadings(editor);
|
||||
});
|
||||
|
||||
return () => {
|
||||
resetHeadings();
|
||||
unsubscribe();
|
||||
};
|
||||
}, [editor, resetHeadings, setHeadings]);
|
||||
};
|
||||
|
||||
@@ -13,11 +13,10 @@ const SAVE_INTERVAL = 60000;
|
||||
export const useSaveDoc = (
|
||||
docId: string,
|
||||
yDoc: Y.Doc,
|
||||
canSave: boolean,
|
||||
isConnectedToCollabServer: boolean,
|
||||
) => {
|
||||
const { mutate: updateDoc } = useUpdateDoc({
|
||||
listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
|
||||
listInvalidQueries: [KEY_LIST_DOC_VERSIONS],
|
||||
onSuccess: () => {
|
||||
setIsLocalChange(false);
|
||||
},
|
||||
@@ -47,7 +46,7 @@ export const useSaveDoc = (
|
||||
}, [yDoc]);
|
||||
|
||||
const saveDoc = useCallback(() => {
|
||||
if (!canSave || !isLocalChange) {
|
||||
if (!isLocalChange) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -58,14 +57,7 @@ export const useSaveDoc = (
|
||||
});
|
||||
|
||||
return true;
|
||||
}, [
|
||||
canSave,
|
||||
isLocalChange,
|
||||
updateDoc,
|
||||
docId,
|
||||
yDoc,
|
||||
isConnectedToCollabServer,
|
||||
]);
|
||||
}, [isLocalChange, updateDoc, docId, yDoc, isConnectedToCollabServer]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ import { useEffect } from 'react';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../types';
|
||||
|
||||
export const useShortcuts = (editor: DocsBlockNoteEditor) => {
|
||||
export const useShortcuts = (
|
||||
editor: DocsBlockNoteEditor,
|
||||
el: HTMLDivElement | null,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === '@' && editor?.isFocused()) {
|
||||
@@ -29,11 +32,14 @@ export const useShortcuts = (editor: DocsBlockNoteEditor) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Attach the event listener to the document instead of the window
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
el.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
el.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [editor]);
|
||||
}, [editor, el]);
|
||||
};
|
||||
|
||||
@@ -1,36 +1,12 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { captureException } from '@sentry/nextjs';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { backendUrl } from '@/api';
|
||||
import { useMediaUrl } from '@/core/config';
|
||||
import { sleep } from '@/utils';
|
||||
|
||||
import { checkDocMediaStatus, useCreateDocAttachment } from '../api';
|
||||
import { useCreateDocAttachment } from '../api';
|
||||
import { DocsBlockNoteEditor } from '../types';
|
||||
|
||||
/**
|
||||
* Upload file can be analyzed on the server side,
|
||||
* we had this function to wait for the analysis to be done
|
||||
* before returning the file url. It will keep the loader
|
||||
* on the upload button until the analysis is done.
|
||||
* @param url
|
||||
* @returns Promise<CheckDocMediaStatusResponse> status_code
|
||||
* @description Waits for the upload to be analyzed by checking the status of the file.
|
||||
*/
|
||||
const loopCheckDocMediaStatus = async (url: string) => {
|
||||
const SLEEP_TIME = 5000;
|
||||
const response = await checkDocMediaStatus({
|
||||
urlMedia: url,
|
||||
});
|
||||
|
||||
if (response.status === 'ready') {
|
||||
return response;
|
||||
} else {
|
||||
await sleep(SLEEP_TIME);
|
||||
return await loopCheckDocMediaStatus(url);
|
||||
}
|
||||
};
|
||||
|
||||
export const useUploadFile = (docId: string) => {
|
||||
const {
|
||||
mutateAsync: createDocAttachment,
|
||||
@@ -63,94 +39,9 @@ export const useUploadFile = (docId: string) => {
|
||||
export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
|
||||
const ANALYZE_URL = 'media-check';
|
||||
const { t } = useTranslation();
|
||||
const mediaUrl = useMediaUrl();
|
||||
const timeoutIds = useRef<Record<string, NodeJS.Timeout>>({});
|
||||
|
||||
const blockAnalyzeProcess = useCallback(
|
||||
(editor: DocsBlockNoteEditor, blockId: string, url: string) => {
|
||||
if (timeoutIds.current[url]) {
|
||||
clearTimeout(timeoutIds.current[url]);
|
||||
}
|
||||
|
||||
// Delay to let the time to the dom to be rendered
|
||||
const timoutId = setTimeout(() => {
|
||||
// Replace the resource block by a loading block
|
||||
const { insertedBlocks, removedBlocks } = editor.replaceBlocks(
|
||||
[blockId],
|
||||
[
|
||||
{
|
||||
type: 'uploadLoader',
|
||||
props: {
|
||||
information: t('Analyzing file...'),
|
||||
type: 'loading',
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
loopCheckDocMediaStatus(url)
|
||||
.then((response) => {
|
||||
if (insertedBlocks.length === 0 || removedBlocks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingBlockId = insertedBlocks[0].id;
|
||||
const removedBlock = removedBlocks[0];
|
||||
|
||||
removedBlock.props = {
|
||||
...removedBlock.props,
|
||||
url: `${mediaUrl}${response.file}`,
|
||||
};
|
||||
|
||||
// Replace the loading block with the resource block (image, audio, video, pdf ...)
|
||||
editor.replaceBlocks([loadingBlockId], [removedBlock]);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error analyzing file:', error);
|
||||
|
||||
const loadingBlock = insertedBlocks[0];
|
||||
|
||||
if (!loadingBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingBlock.props = {
|
||||
...loadingBlock.props,
|
||||
type: 'warning',
|
||||
information: t(
|
||||
'The antivirus has detected an anomaly in your file.',
|
||||
),
|
||||
};
|
||||
|
||||
editor.updateBlock(loadingBlock.id, loadingBlock);
|
||||
});
|
||||
}, 250);
|
||||
|
||||
timeoutIds.current[url] = timoutId;
|
||||
},
|
||||
[t, mediaUrl],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const blocksAnalyze = editor?.document.filter(
|
||||
(block) => 'url' in block.props && block.props.url.includes(ANALYZE_URL),
|
||||
);
|
||||
|
||||
if (!blocksAnalyze?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
blocksAnalyze.forEach((block) => {
|
||||
if (!('url' in block.props)) {
|
||||
return;
|
||||
}
|
||||
|
||||
blockAnalyzeProcess(editor, block.id, block.props.url);
|
||||
});
|
||||
}, [blockAnalyzeProcess, editor]);
|
||||
|
||||
useEffect(() => {
|
||||
editor.onChange((_, context) => {
|
||||
const unsubscribe = editor.onChange((_, context) => {
|
||||
const blocksChanges = context.getChanges();
|
||||
|
||||
if (!blocksChanges.length) {
|
||||
@@ -169,11 +60,49 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
|
||||
return;
|
||||
}
|
||||
|
||||
blockAnalyzeProcess(
|
||||
editor,
|
||||
blockChanges.block.id,
|
||||
blockChanges.block.props.url,
|
||||
);
|
||||
const blockUploadUrl = blockChanges.block.props.url;
|
||||
const blockUploadType = blockChanges.block.type;
|
||||
const blockUploadName = blockChanges.block.props.name;
|
||||
const blockUploadShowPreview =
|
||||
('showPreview' in blockChanges.block.props &&
|
||||
blockChanges.block.props.showPreview) ||
|
||||
false;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
// Replace the resource block by a uploadLoader block
|
||||
// to show analyzing status
|
||||
try {
|
||||
editor.replaceBlocks(
|
||||
[blockChanges.block.id],
|
||||
[
|
||||
{
|
||||
type: 'uploadLoader',
|
||||
props: {
|
||||
information: t('Analyzing file...'),
|
||||
type: 'loading',
|
||||
blockUploadName,
|
||||
blockUploadType,
|
||||
blockUploadUrl,
|
||||
blockUploadShowPreview,
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
} catch (error) {
|
||||
captureException(error, {
|
||||
extra: { info: 'Error replacing block for upload loader' },
|
||||
});
|
||||
}
|
||||
}, 250);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
unsubscribe();
|
||||
};
|
||||
});
|
||||
}, [blockAnalyzeProcess, mediaUrl, editor, t]);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [editor, t]);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { css } from 'styled-components';
|
||||
|
||||
export const cssEditor = (readonly: boolean, isDeletedDoc: boolean) => css`
|
||||
export const cssEditor = css`
|
||||
&,
|
||||
& > .bn-container,
|
||||
& .ProseMirror {
|
||||
height: 100%;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
& .ProseMirror {
|
||||
/**
|
||||
* WCAG Accessibility contrast fixes for BlockNote editor
|
||||
*/
|
||||
@@ -16,6 +17,9 @@ export const cssEditor = (readonly: boolean, isDeletedDoc: boolean) => css`
|
||||
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;
|
||||
@@ -29,14 +33,16 @@ export const cssEditor = (readonly: boolean, isDeletedDoc: boolean) => css`
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bn-side-menu .mantine-UnstyledButton-root svg {
|
||||
color: #767676 !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -87,6 +93,9 @@ export const cssEditor = (readonly: boolean, isDeletedDoc: boolean) => css`
|
||||
.bn-side-menu[data-block-type='divider'] {
|
||||
height: 38px;
|
||||
}
|
||||
.bn-side-menu .mantine-UnstyledButton-root svg {
|
||||
color: #767676 !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callout, Paragraph and Heading blocks
|
||||
@@ -94,21 +103,17 @@ export const cssEditor = (readonly: boolean, isDeletedDoc: boolean) => css`
|
||||
.bn-block {
|
||||
border-radius: var(--c--theme--spacings--3xs);
|
||||
}
|
||||
|
||||
.bn-block-outer {
|
||||
border-radius: var(--c--theme--spacings--3xs);
|
||||
}
|
||||
|
||||
.bn-block[data-background-color] > .bn-block-content {
|
||||
.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;
|
||||
}
|
||||
@@ -127,13 +132,6 @@ export const cssEditor = (readonly: boolean, isDeletedDoc: boolean) => css`
|
||||
.bn-block-outer:not([data-prev-depth-changed]):before {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
${isDeletedDoc &&
|
||||
`
|
||||
.node-interlinkingLinkInline button {
|
||||
pointer-events: none;
|
||||
}
|
||||
`}
|
||||
}
|
||||
|
||||
& .bn-editor {
|
||||
@@ -146,6 +144,16 @@ export const cssEditor = (readonly: boolean, isDeletedDoc: boolean) => css`
|
||||
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) {
|
||||
@@ -173,8 +181,10 @@ export const cssEditor = (readonly: boolean, isDeletedDoc: boolean) => css`
|
||||
}
|
||||
|
||||
@media screen and (width <= 560px) {
|
||||
.--docs--doc-readonly & .bn-editor {
|
||||
padding-left: 10px;
|
||||
}
|
||||
& .bn-editor {
|
||||
${readonly && `padding-left: 10px;`}
|
||||
padding-right: 10px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='heading'][data-level='1'] {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Paragraph } from 'docx';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { DocsExporterDocx } from '../types';
|
||||
|
||||
export const blockMappingDividerDocx: DocsExporterDocx['mappings']['blockMapping']['divider'] =
|
||||
() => {
|
||||
const { colorsTokens } = useCunninghamTheme.getState();
|
||||
|
||||
return new Paragraph({
|
||||
spacing: {
|
||||
before: 200,
|
||||
},
|
||||
border: {
|
||||
top: {
|
||||
color: colorsTokens['greyscale-300'],
|
||||
size: 1,
|
||||
style: 'single',
|
||||
space: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Text } from '@react-pdf/renderer';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { DocsExporterPDF } from '../types';
|
||||
|
||||
export const blockMappingDividerPDF: DocsExporterPDF['mappings']['blockMapping']['divider'] =
|
||||
() => {
|
||||
const { colorsTokens } = useCunninghamTheme.getState();
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
marginVertical: 10,
|
||||
backgroundColor: colorsTokens['greyscale-300'],
|
||||
height: '2px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -100,16 +100,13 @@ function blockPropsToStyles(
|
||||
? undefined
|
||||
: {
|
||||
type: ShadingType.SOLID,
|
||||
color:
|
||||
colors[
|
||||
props.backgroundColor as keyof typeof colors
|
||||
].background.slice(1),
|
||||
color: colors[props.backgroundColor].background.slice(1),
|
||||
},
|
||||
run:
|
||||
props.textColor === 'default' || !props.textColor
|
||||
? undefined
|
||||
: {
|
||||
color: colors[props.textColor as keyof typeof colors].text.slice(1),
|
||||
color: colors[props.textColor].text.slice(1),
|
||||
},
|
||||
alignment:
|
||||
!props.textAlignment || props.textAlignment === 'left'
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
export * from './calloutDocx';
|
||||
export * from './calloutPDF';
|
||||
export * from './dividerDocx';
|
||||
export * from './dividerPDF';
|
||||
export * from './headingPDF';
|
||||
export * from './imageDocx';
|
||||
export * from './imagePDF';
|
||||
|
||||
@@ -92,15 +92,11 @@ export const blockMappingTablePDF: DocsExporterPDF['mappings']['blockMapping']['
|
||||
color:
|
||||
cellProps.textColor === 'default'
|
||||
? undefined
|
||||
: options.colors[
|
||||
cellProps.textColor as keyof typeof options.colors
|
||||
].text,
|
||||
: options.colors[cellProps.textColor].text,
|
||||
backgroundColor:
|
||||
cellProps.backgroundColor === 'default'
|
||||
? undefined
|
||||
: options.colors[
|
||||
cellProps.backgroundColor as keyof typeof options.colors
|
||||
].background,
|
||||
: options.colors[cellProps.backgroundColor].background,
|
||||
textAlign: cellProps.textAlignment,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { docxDefaultSchemaMappings } from '@blocknote/xl-docx-exporter';
|
||||
import { Paragraph } from 'docx';
|
||||
import { TextRun } from 'docx';
|
||||
|
||||
import {
|
||||
blockMappingCalloutDocx,
|
||||
blockMappingDividerDocx,
|
||||
blockMappingImageDocx,
|
||||
blockMappingQuoteDocx,
|
||||
blockMappingUploadLoaderDocx,
|
||||
@@ -16,9 +15,8 @@ export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
|
||||
blockMapping: {
|
||||
...docxDefaultSchemaMappings.blockMapping,
|
||||
callout: blockMappingCalloutDocx,
|
||||
divider: blockMappingDividerDocx,
|
||||
// We're using the file block mapping for PDF blocks
|
||||
// The types don't match exactly but the implementation is compatible
|
||||
// We're reusing the file block mapping for PDF blocks; both share the same
|
||||
// implementation signature, so we can reuse the handler directly.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
pdf: docxDefaultSchemaMappings.blockMapping.file as any,
|
||||
quote: blockMappingQuoteDocx,
|
||||
@@ -27,7 +25,7 @@ export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
|
||||
},
|
||||
inlineContentMapping: {
|
||||
...docxDefaultSchemaMappings.inlineContentMapping,
|
||||
interlinkingSearchInline: () => new Paragraph(''),
|
||||
interlinkingSearchInline: () => new TextRun(''),
|
||||
interlinkingLinkInline: inlineContentMappingInterlinkingLinkDocx,
|
||||
},
|
||||
styleMapping: {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { pdfDefaultSchemaMappings } from '@blocknote/xl-pdf-exporter';
|
||||
|
||||
import {
|
||||
blockMappingCalloutPDF,
|
||||
blockMappingDividerPDF,
|
||||
blockMappingHeadingPDF,
|
||||
blockMappingImagePDF,
|
||||
blockMappingParagraphPDF,
|
||||
@@ -21,7 +20,6 @@ export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = {
|
||||
heading: blockMappingHeadingPDF,
|
||||
image: blockMappingImagePDF,
|
||||
paragraph: blockMappingParagraphPDF,
|
||||
divider: blockMappingDividerPDF,
|
||||
quote: blockMappingQuotePDF,
|
||||
table: blockMappingTablePDF,
|
||||
// We're using the file block mapping for PDF blocks
|
||||
|
||||
@@ -76,16 +76,13 @@ export function docxBlockPropsToStyles(
|
||||
? undefined
|
||||
: {
|
||||
type: ShadingType.SOLID,
|
||||
color:
|
||||
colors[
|
||||
props.backgroundColor as keyof typeof colors
|
||||
].background.slice(1),
|
||||
color: colors[props.backgroundColor].background.slice(1),
|
||||
},
|
||||
run:
|
||||
props.textColor === 'default' || !props.textColor
|
||||
? undefined
|
||||
: {
|
||||
color: colors[props.textColor as keyof typeof colors].text.slice(1),
|
||||
color: colors[props.textColor].text.slice(1),
|
||||
},
|
||||
alignment:
|
||||
!props.textAlignment || props.textAlignment === 'left'
|
||||
|
||||
@@ -76,7 +76,7 @@ export const BoutonShare = ({
|
||||
|
||||
return (
|
||||
<Button
|
||||
color="tertiary-text"
|
||||
color="primary-text"
|
||||
onClick={open}
|
||||
size="medium"
|
||||
disabled={isDisabled}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
|
||||
import { Tooltip } from '@openfun/cunningham-react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -8,14 +7,16 @@ import { Box, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import {
|
||||
Doc,
|
||||
KEY_DOC,
|
||||
KEY_LIST_DOC,
|
||||
DocIcon,
|
||||
getEmojiAndTitle,
|
||||
useDocStore,
|
||||
useDocTitleUpdate,
|
||||
useDocUtils,
|
||||
useIsCollaborativeEditable,
|
||||
useTrans,
|
||||
useUpdateDoc,
|
||||
} from '@/docs/doc-management';
|
||||
import { useBroadcastStore, useResponsiveStore } from '@/stores';
|
||||
import SimpleFileIcon from '@/features/docs/doc-management/assets/simple-document.svg';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
interface DocTitleProps {
|
||||
doc: Doc;
|
||||
@@ -49,52 +50,90 @@ export const DocTitleText = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const DocTitleEmojiPicker = ({ doc }: DocTitleProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { emoji } = getEmojiAndTitle(doc.title ?? '');
|
||||
|
||||
if (!emoji) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={t('Edit document emoji')}
|
||||
aria-hidden={true}
|
||||
placement="top"
|
||||
>
|
||||
<Box
|
||||
$css={css`
|
||||
padding: 4px;
|
||||
padding-top: 3px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: ${colorsTokens['greyscale-100']};
|
||||
border-radius: 4px;
|
||||
}
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
`}
|
||||
>
|
||||
<DocIcon
|
||||
buttonProps={{
|
||||
$width: '32px',
|
||||
$height: '32px',
|
||||
$justify: 'space-between',
|
||||
$align: 'center',
|
||||
}}
|
||||
withEmojiPicker={doc.abilities.partial_update}
|
||||
docId={doc.id}
|
||||
title={doc.title}
|
||||
emoji={emoji}
|
||||
$size="23px"
|
||||
defaultIcon={
|
||||
<SimpleFileIcon
|
||||
width="25px"
|
||||
height="25px"
|
||||
aria-hidden="true"
|
||||
aria-label={t('Simple document icon')}
|
||||
color={colorsTokens['primary-500']}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const [titleDisplay, setTitleDisplay] = useState(doc.title);
|
||||
const treeContext = useTreeContext<Doc>();
|
||||
|
||||
const { isTopRoot } = useDocUtils(doc);
|
||||
const { untitledDocument } = useTrans();
|
||||
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(doc.title ?? '');
|
||||
const [titleDisplay, setTitleDisplay] = useState(
|
||||
isTopRoot ? doc.title : titleWithoutEmoji,
|
||||
);
|
||||
|
||||
const { broadcast } = useBroadcastStore();
|
||||
|
||||
const { mutate: updateDoc } = useUpdateDoc({
|
||||
listInvalideQueries: [KEY_DOC, KEY_LIST_DOC],
|
||||
onSuccess(updatedDoc) {
|
||||
// Broadcast to every user connected to the document
|
||||
broadcast(`${KEY_DOC}-${updatedDoc.id}`);
|
||||
|
||||
if (!treeContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (treeContext.root?.id === updatedDoc.id) {
|
||||
treeContext?.setRoot(updatedDoc);
|
||||
} else {
|
||||
treeContext?.treeData.updateNode(updatedDoc.id, updatedDoc);
|
||||
}
|
||||
},
|
||||
});
|
||||
const { updateDocTitle } = useDocTitleUpdate();
|
||||
|
||||
const handleTitleSubmit = useCallback(
|
||||
(inputText: string) => {
|
||||
let sanitizedTitle = inputText.trim();
|
||||
sanitizedTitle = sanitizedTitle.replace(/(\r\n|\n|\r)/gm, '');
|
||||
|
||||
// When blank we set to untitled
|
||||
if (!sanitizedTitle) {
|
||||
setTitleDisplay('');
|
||||
}
|
||||
|
||||
// If mutation we update
|
||||
if (sanitizedTitle !== doc.title) {
|
||||
if (isTopRoot) {
|
||||
const sanitizedTitle = updateDocTitle(doc, inputText);
|
||||
setTitleDisplay(sanitizedTitle);
|
||||
updateDoc({ id: doc.id, title: sanitizedTitle });
|
||||
} else {
|
||||
const sanitizedTitle = updateDocTitle(
|
||||
doc,
|
||||
emoji ? `${emoji} ${inputText}` : inputText,
|
||||
);
|
||||
const { titleWithoutEmoji: sanitizedTitleWithoutEmoji } =
|
||||
getEmojiAndTitle(sanitizedTitle);
|
||||
|
||||
setTitleDisplay(sanitizedTitleWithoutEmoji);
|
||||
}
|
||||
},
|
||||
[doc.id, doc.title, updateDoc],
|
||||
[updateDocTitle, doc, emoji, isTopRoot],
|
||||
);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
@@ -105,43 +144,52 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTitleDisplay(doc.title);
|
||||
}, [doc]);
|
||||
setTitleDisplay(isTopRoot ? doc.title : titleWithoutEmoji);
|
||||
}, [doc.title, isTopRoot, titleWithoutEmoji]);
|
||||
|
||||
return (
|
||||
<Tooltip content={t('Rename')} aria-hidden={true} placement="top">
|
||||
<Box
|
||||
as="span"
|
||||
role="textbox"
|
||||
className="--docs--doc-title-input"
|
||||
contentEditable
|
||||
defaultValue={titleDisplay || undefined}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
suppressContentEditableWarning={true}
|
||||
aria-label={`${t('Document title')}`}
|
||||
aria-multiline={false}
|
||||
onBlurCapture={(event) =>
|
||||
handleTitleSubmit(event.target.textContent || '')
|
||||
}
|
||||
$color={colorsTokens['greyscale-1000']}
|
||||
$minHeight="40px"
|
||||
$padding={{ right: 'big' }}
|
||||
$css={css`
|
||||
&[contenteditable='true']:empty:not(:focus):before {
|
||||
content: '${untitledDocument}';
|
||||
color: grey;
|
||||
pointer-events: none;
|
||||
font-style: italic;
|
||||
<Box
|
||||
className="--docs--doc-title"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="4px"
|
||||
$minHeight="40px"
|
||||
>
|
||||
{!isTopRoot && <DocTitleEmojiPicker doc={doc} />}
|
||||
|
||||
<Tooltip content={t('Rename')} aria-hidden={true} placement="top">
|
||||
<Box
|
||||
as="span"
|
||||
role="textbox"
|
||||
className="--docs--doc-title-input"
|
||||
contentEditable
|
||||
defaultValue={titleDisplay || undefined}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
suppressContentEditableWarning={true}
|
||||
aria-label={`${t('Document title')}`}
|
||||
aria-multiline={false}
|
||||
onBlurCapture={(event) =>
|
||||
handleTitleSubmit(event.target.textContent || '')
|
||||
}
|
||||
font-size: ${isDesktop
|
||||
? css`var(--c--theme--font--sizes--h2)`
|
||||
: css`var(--c--theme--font--sizes--sm)`};
|
||||
font-weight: 700;
|
||||
outline: none;
|
||||
`}
|
||||
>
|
||||
{titleDisplay}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
$color={colorsTokens['greyscale-1000']}
|
||||
$padding={{ right: 'big' }}
|
||||
$css={css`
|
||||
&[contenteditable='true']:empty:not(:focus):before {
|
||||
content: '${untitledDocument}';
|
||||
color: grey;
|
||||
pointer-events: none;
|
||||
font-style: italic;
|
||||
}
|
||||
font-size: ${isDesktop
|
||||
? css`var(--c--theme--font--sizes--h2)`
|
||||
: css`var(--c--theme--font--sizes--sm)`};
|
||||
font-weight: 700;
|
||||
outline: none;
|
||||
`}
|
||||
>
|
||||
{titleDisplay}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,9 +20,11 @@ import {
|
||||
KEY_DOC,
|
||||
KEY_LIST_DOC,
|
||||
ModalRemoveDoc,
|
||||
getEmojiAndTitle,
|
||||
useCopyDocLink,
|
||||
useCreateFavoriteDoc,
|
||||
useDeleteFavoriteDoc,
|
||||
useDocTitleUpdate,
|
||||
useDocUtils,
|
||||
useDuplicateDoc,
|
||||
} from '@/docs/doc-management';
|
||||
@@ -49,7 +51,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
const treeContext = useTreeContext<Doc>();
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
const { isChild } = useDocUtils(doc);
|
||||
const { isChild, isTopRoot } = useDocUtils(doc);
|
||||
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
|
||||
@@ -58,7 +60,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
const selectHistoryModal = useModal();
|
||||
const modalShare = useModal();
|
||||
|
||||
const { isSmallMobile, isDesktop } = useResponsiveStore();
|
||||
const { isSmallMobile, isMobile } = useResponsiveStore();
|
||||
const copyDocLink = useCopyDocLink(doc.id);
|
||||
const { mutate: duplicateDoc } = useDuplicateDoc({
|
||||
onSuccess: (data) => {
|
||||
@@ -67,10 +69,10 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
});
|
||||
const { isFeatureFlagActivated } = useAnalytics();
|
||||
const removeFavoriteDoc = useDeleteFavoriteDoc({
|
||||
listInvalideQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||
});
|
||||
const makeFavoriteDoc = useCreateFavoriteDoc({
|
||||
listInvalideQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -83,29 +85,25 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
});
|
||||
}, [selectHistoryModal.isOpen, queryClient]);
|
||||
|
||||
// Emoji Management
|
||||
const { emoji } = getEmojiAndTitle(doc.title ?? '');
|
||||
const { updateDocEmoji } = useDocTitleUpdate();
|
||||
|
||||
const options: DropdownMenuOption[] = [
|
||||
...(isSmallMobile
|
||||
? [
|
||||
{
|
||||
label: t('Share'),
|
||||
icon: 'group',
|
||||
callback: modalShare.open,
|
||||
},
|
||||
{
|
||||
label: t('Export'),
|
||||
icon: 'download',
|
||||
callback: () => {
|
||||
setIsModalExportOpen(true);
|
||||
},
|
||||
show: !!ModalExport,
|
||||
},
|
||||
{
|
||||
label: t('Copy link'),
|
||||
icon: 'add_link',
|
||||
callback: copyDocLink,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: t('Share'),
|
||||
icon: 'group',
|
||||
callback: modalShare.open,
|
||||
show: isSmallMobile,
|
||||
},
|
||||
{
|
||||
label: t('Export'),
|
||||
icon: 'download',
|
||||
callback: () => {
|
||||
setIsModalExportOpen(true);
|
||||
},
|
||||
show: !!ModalExport && isSmallMobile,
|
||||
},
|
||||
{
|
||||
label: doc.is_favorite ? t('Unpin') : t('Pin'),
|
||||
icon: 'push_pin',
|
||||
@@ -125,7 +123,31 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
callback: () => {
|
||||
selectHistoryModal.open();
|
||||
},
|
||||
show: isDesktop,
|
||||
show: !isMobile,
|
||||
showSeparator: isTopRoot ? true : false,
|
||||
},
|
||||
{
|
||||
label: t('Remove emoji'),
|
||||
icon: 'emoji_emotions',
|
||||
callback: () => {
|
||||
updateDocEmoji(doc.id, doc.title ?? '', '');
|
||||
},
|
||||
showSeparator: true,
|
||||
show: !!emoji && doc.abilities.partial_update && !isTopRoot,
|
||||
},
|
||||
{
|
||||
label: t('Add emoji'),
|
||||
icon: 'emoji_emotions',
|
||||
callback: () => {
|
||||
updateDocEmoji(doc.id, doc.title ?? '', '📄');
|
||||
},
|
||||
showSeparator: true,
|
||||
show: !emoji && doc.abilities.partial_update && !isTopRoot,
|
||||
},
|
||||
{
|
||||
label: t('Copy link'),
|
||||
icon: 'add_link',
|
||||
callback: copyDocLink,
|
||||
},
|
||||
{
|
||||
label: t('Copy as {{format}}', { format: 'Markdown' }),
|
||||
@@ -141,6 +163,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
void copyCurrentEditorToClipboard('html');
|
||||
},
|
||||
show: isFeatureFlagActivated('CopyAsHTML'),
|
||||
showSeparator: true,
|
||||
},
|
||||
{
|
||||
label: t('Duplicate'),
|
||||
@@ -153,6 +176,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
canSave: doc.abilities.partial_update,
|
||||
});
|
||||
},
|
||||
showSeparator: true,
|
||||
},
|
||||
{
|
||||
label: isChild ? t('Delete sub-document') : t('Delete document'),
|
||||
@@ -207,25 +231,22 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
aria-label={t('Export the document')}
|
||||
/>
|
||||
)}
|
||||
<DropdownMenu options={options} label={t('Open the document options')}>
|
||||
<IconOptions
|
||||
aria-hidden="true"
|
||||
isHorizontal
|
||||
$theme="primary"
|
||||
$padding={{ all: 'xs' }}
|
||||
$css={css`
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
background-color: ${colorsTokens['greyscale-100']};
|
||||
}
|
||||
${isSmallMobile
|
||||
? css`
|
||||
padding: 10px;
|
||||
border: 1px solid ${colorsTokens['greyscale-300']};
|
||||
`
|
||||
: ''}
|
||||
`}
|
||||
/>
|
||||
<DropdownMenu
|
||||
options={options}
|
||||
label={t('Open the document options')}
|
||||
buttonCss={css`
|
||||
padding: ${spacingsTokens['xs']};
|
||||
&:hover {
|
||||
background-color: ${colorsTokens['greyscale-100']};
|
||||
}
|
||||
${isSmallMobile
|
||||
? css`
|
||||
border: 1px solid ${colorsTokens['greyscale-300']};
|
||||
`
|
||||
: ''}
|
||||
`}
|
||||
>
|
||||
<IconOptions aria-hidden="true" isHorizontal $theme="primary" />
|
||||
</DropdownMenu>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './DocHeader';
|
||||
export * from './DocVersionHeader';
|
||||
export * from './DocTitle';
|
||||
|
||||
+15
-27
@@ -10,13 +10,16 @@ import {
|
||||
getEmojiAndTitle,
|
||||
} from '../utils';
|
||||
|
||||
// Mock Y.js
|
||||
vi.mock('yjs', () => ({
|
||||
Doc: vi.fn().mockImplementation(() => ({
|
||||
getXmlFragment: vi.fn().mockReturnValue('mocked-xml-fragment'),
|
||||
})),
|
||||
applyUpdate: vi.fn(),
|
||||
}));
|
||||
vi.mock('yjs', () => {
|
||||
class MockDoc {
|
||||
getXmlFragment = vi.fn().mockReturnValue('mocked-xml-fragment');
|
||||
}
|
||||
|
||||
return {
|
||||
Doc: MockDoc,
|
||||
applyUpdate: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('doc-management utils', () => {
|
||||
beforeEach(() => {
|
||||
@@ -26,45 +29,30 @@ describe('doc-management utils', () => {
|
||||
describe('base64ToYDoc', () => {
|
||||
it('should convert base64 string to Y.Doc', () => {
|
||||
const base64String = 'dGVzdA=='; // "test" in base64
|
||||
const mockYDoc = { getXmlFragment: vi.fn() };
|
||||
|
||||
(Y.Doc as any).mockReturnValue(mockYDoc);
|
||||
|
||||
const result = base64ToYDoc(base64String);
|
||||
|
||||
expect(Y.Doc).toHaveBeenCalled();
|
||||
expect(Y.applyUpdate).toHaveBeenCalledWith(mockYDoc, expect.any(Buffer));
|
||||
expect(result).toBe(mockYDoc);
|
||||
expect(result).toBeInstanceOf(Y.Doc);
|
||||
expect(Y.applyUpdate).toHaveBeenCalledWith(result, expect.any(Buffer));
|
||||
});
|
||||
|
||||
it('should handle empty base64 string', () => {
|
||||
const base64String = '';
|
||||
const mockYDoc = { getXmlFragment: vi.fn() };
|
||||
|
||||
(Y.Doc as any).mockReturnValue(mockYDoc);
|
||||
|
||||
const result = base64ToYDoc(base64String);
|
||||
|
||||
expect(Y.Doc).toHaveBeenCalled();
|
||||
expect(Y.applyUpdate).toHaveBeenCalledWith(mockYDoc, expect.any(Buffer));
|
||||
expect(result).toBe(mockYDoc);
|
||||
expect(result).toBeInstanceOf(Y.Doc);
|
||||
expect(Y.applyUpdate).toHaveBeenCalledWith(result, expect.any(Buffer));
|
||||
});
|
||||
});
|
||||
|
||||
describe('base64ToBlocknoteXmlFragment', () => {
|
||||
it('should convert base64 to Blocknote XML fragment', () => {
|
||||
const base64String = 'dGVzdA==';
|
||||
const mockYDoc = {
|
||||
getXmlFragment: vi.fn().mockReturnValue('mocked-xml-fragment'),
|
||||
};
|
||||
|
||||
(Y.Doc as any).mockReturnValue(mockYDoc);
|
||||
|
||||
const result = base64ToBlocknoteXmlFragment(base64String);
|
||||
|
||||
expect(Y.Doc).toHaveBeenCalled();
|
||||
expect(Y.applyUpdate).toHaveBeenCalledWith(mockYDoc, expect.any(Buffer));
|
||||
expect(mockYDoc.getXmlFragment).toHaveBeenCalledWith('document-store');
|
||||
expect(Y.applyUpdate).toHaveBeenCalled();
|
||||
expect(result).toBe('mocked-xml-fragment');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,4 +9,3 @@ export * from './useDuplicateDoc';
|
||||
export * from './useRestoreDoc';
|
||||
export * from './useSubDocs';
|
||||
export * from './useUpdateDoc';
|
||||
export * from './useUpdateDocLink';
|
||||
|
||||
@@ -20,9 +20,10 @@ export const createDoc = async (): Promise<Doc> => {
|
||||
|
||||
interface CreateDocProps {
|
||||
onSuccess: (data: Doc) => void;
|
||||
onError?: (error: APIError) => void;
|
||||
}
|
||||
|
||||
export function useCreateDoc({ onSuccess }: CreateDocProps) {
|
||||
export function useCreateDoc({ onSuccess, onError }: CreateDocProps) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<Doc, APIError>({
|
||||
mutationFn: createDoc,
|
||||
@@ -32,5 +33,8 @@ export function useCreateDoc({ onSuccess }: CreateDocProps) {
|
||||
});
|
||||
onSuccess(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
onError?.(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+3
-3
@@ -21,18 +21,18 @@ export const createFavoriteDoc = async ({ id }: CreateFavoriteDocParams) => {
|
||||
|
||||
interface CreateFavoriteDocProps {
|
||||
onSuccess?: () => void;
|
||||
listInvalideQueries?: string[];
|
||||
listInvalidQueries?: string[];
|
||||
}
|
||||
|
||||
export function useCreateFavoriteDoc({
|
||||
onSuccess,
|
||||
listInvalideQueries,
|
||||
listInvalidQueries,
|
||||
}: CreateFavoriteDocProps) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, APIError, CreateFavoriteDocParams>({
|
||||
mutationFn: createFavoriteDoc,
|
||||
onSuccess: () => {
|
||||
listInvalideQueries?.forEach((queryKey) => {
|
||||
listInvalidQueries?.forEach((queryKey) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [queryKey],
|
||||
});
|
||||
|
||||
+3
-3
@@ -21,18 +21,18 @@ export const deleteFavoriteDoc = async ({ id }: DeleteFavoriteDocParams) => {
|
||||
|
||||
interface DeleteFavoriteDocProps {
|
||||
onSuccess?: () => void;
|
||||
listInvalideQueries?: string[];
|
||||
listInvalidQueries?: string[];
|
||||
}
|
||||
|
||||
export function useDeleteFavoriteDoc({
|
||||
onSuccess,
|
||||
listInvalideQueries,
|
||||
listInvalidQueries,
|
||||
}: DeleteFavoriteDocProps) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, APIError, DeleteFavoriteDocParams>({
|
||||
mutationFn: deleteFavoriteDoc,
|
||||
onSuccess: () => {
|
||||
listInvalideQueries?.forEach((queryKey) => {
|
||||
listInvalidQueries?.forEach((queryKey) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [queryKey],
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ export function useDuplicateDoc(options?: DuplicateDocOptions) {
|
||||
const { provider } = useProviderStore();
|
||||
|
||||
const { mutateAsync: updateDoc } = useUpdateDoc({
|
||||
listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
|
||||
listInvalidQueries: [KEY_LIST_DOC_VERSIONS],
|
||||
});
|
||||
|
||||
return useMutation<DuplicateDocResponse, APIError, DuplicateDocParams>({
|
||||
@@ -80,7 +80,7 @@ export function useDuplicateDoc(options?: DuplicateDocOptions) {
|
||||
|
||||
return await duplicateDoc(variables);
|
||||
},
|
||||
onSuccess: (data, variables, context) => {
|
||||
onSuccess: (data, variables, onMutateResult, context) => {
|
||||
void queryClient.resetQueries({
|
||||
queryKey: [KEY_LIST_DOC],
|
||||
});
|
||||
@@ -89,14 +89,14 @@ export function useDuplicateDoc(options?: DuplicateDocOptions) {
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
void options?.onSuccess?.(data, variables, context);
|
||||
void options?.onSuccess?.(data, variables, onMutateResult, context);
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
onError: (error, variables, onMutateResult, context) => {
|
||||
toast(t('Failed to duplicate the document...'), VariantType.ERROR, {
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
void options?.onError?.(error, variables, context);
|
||||
void options?.onError?.(error, variables, onMutateResult, context);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user