mirror of
https://github.com/suitenumerique/docs.git
synced 2026-05-06 15:12:27 +02:00
Compare commits
37 Commits
improve-in
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0379bbaab5 | ||
|
|
f166e75921 | ||
|
|
f4ded8ee55 | ||
|
|
05423d4f04 | ||
|
|
6691167a40 | ||
|
|
79e909cf64 | ||
|
|
03c049f59f | ||
|
|
43d486610b | ||
|
|
7d24af8702 | ||
|
|
7f9869f547 | ||
|
|
210c8b5660 | ||
|
|
f7bea69d27 | ||
|
|
0df960bd5e | ||
|
|
7427fdd222 | ||
|
|
641c6f43c6 | ||
|
|
e7cbe24f3d | ||
|
|
acb20a0d26 | ||
|
|
cbe6a67704 | ||
|
|
f91223fe4a | ||
|
|
330096eb47 | ||
|
|
ff995c6cd9 | ||
|
|
2e4a1b8ff9 | ||
|
|
004d637c8b | ||
|
|
8a0330a30f | ||
|
|
677392b89b | ||
|
|
b8e1d12aea | ||
|
|
525d8c8417 | ||
|
|
c886cbb41d | ||
|
|
98f3ca2763 | ||
|
|
fb92a43755 | ||
|
|
03fd1fe50e | ||
|
|
fc803226ac | ||
|
|
fb725edda3 | ||
|
|
6838b387a2 | ||
|
|
87f570582f | ||
|
|
37f56fcc22 | ||
|
|
19aa3a36bc |
38
CHANGELOG.md
38
CHANGELOG.md
@@ -6,13 +6,40 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- 🔧(backend) settings CONVERSION_UPLOAD_ENABLED to control usage of docspec
|
||||
|
||||
### Changed
|
||||
|
||||
- ♿(frontend) use aria-haspopup menu on DropButton triggers #2126
|
||||
- ♿️(frontend) add contextual browser tab titles for docs routes #2120
|
||||
- ♿️(frontend) fix empty heading before section titles in HTML export #2125
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) fix tree pagination #2145
|
||||
|
||||
|
||||
## [v4.8.4] - 2026-03-25
|
||||
|
||||
### Added
|
||||
|
||||
- 🚸(frontend) hint min char search users #2064
|
||||
|
||||
### Changed
|
||||
|
||||
- ✨(backend) improve indexing command
|
||||
- checkpoint recovery
|
||||
- asynchronicity
|
||||
- admin command trigger
|
||||
- 💄(frontend) improve comments highlights #1961
|
||||
- ♿️(frontend) improve BoxButton a11y and native button semantics #2103
|
||||
- ♿️(frontend) improve language picker accessibility #2069
|
||||
- ♿️(frontend) add aria-hidden to decorative icons in dropdown menu #2093
|
||||
- 🐛(backend) move lock table closer to the insert operation targeted
|
||||
- ♿️(frontend) replace ARIA grid pattern with list in docs grid #2131
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(y-provider) destroy Y.Doc instances after each convert request #2129
|
||||
- 🐛(backend) remove deleted sub documents in favorite_list endpoint #2083
|
||||
|
||||
## [v4.8.3] - 2026-03-23
|
||||
|
||||
@@ -1181,7 +1208,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/v4.8.3...main
|
||||
[unreleased]: https://github.com/suitenumerique/docs/compare/v4.8.4...main
|
||||
[v4.8.4]: https://github.com/suitenumerique/docs/releases/v4.8.4
|
||||
[v4.8.3]: https://github.com/suitenumerique/docs/releases/v4.8.3
|
||||
[v4.8.2]: https://github.com/suitenumerique/docs/releases/v4.8.2
|
||||
[v4.8.1]: https://github.com/suitenumerique/docs/releases/v4.8.1
|
||||
|
||||
2
Makefile
2
Makefile
@@ -260,7 +260,7 @@ demo: ## flush db then create a demo for load testing purpose
|
||||
.PHONY: demo
|
||||
|
||||
index: ## index all documents to remote search
|
||||
@$(MANAGE) index $(args)
|
||||
@$(MANAGE) index
|
||||
.PHONY: index
|
||||
|
||||
# Nota bene: Black should come after isort just in case they don't agree...
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
# Index Command
|
||||
|
||||
The `index` management command is used to index documents to the remote search indexer.
|
||||
|
||||
## Usage
|
||||
|
||||
### Make Command
|
||||
|
||||
```bash
|
||||
# Basic usage with defaults
|
||||
make index
|
||||
|
||||
# With custom parameters
|
||||
make index args="--batch-size 100 --lower-time-bound 2024-01-01T00:00:00 --upper-time-bound 2026-01-01T00:00:00"
|
||||
|
||||
```
|
||||
|
||||
### Command line
|
||||
|
||||
```bash
|
||||
python manage.py index \
|
||||
--lower-time-bound "2024-01-01T00:00:00" \
|
||||
--upper-time-bound "2024-01-31T23:59:59" \
|
||||
--batch-size 200 \
|
||||
--async
|
||||
```
|
||||
|
||||
### Django Admin
|
||||
|
||||
The command is available in the Django admin interface:
|
||||
|
||||
1. Go to `/admin/run-indexing/`, you arrive at the "Run Indexing Command" page
|
||||
2. Fill in the form with the desired parameters
|
||||
3. Click **"Run Indexing Command"**
|
||||
|
||||
## Parameters
|
||||
|
||||
### `--batch-size`
|
||||
- **type:** Integer
|
||||
- **default:** `settings.SEARCH_INDEXER_BATCH_SIZE`
|
||||
- **description:** Number of documents to process per batch. Higher values may improve performance but use more memory.
|
||||
|
||||
### `--lower-time-bound`
|
||||
- **optional**: true
|
||||
- **type:** ISO 8601 datetime string
|
||||
- **default:** `None`
|
||||
- **description:** Only documents updated after this date will be indexed.
|
||||
|
||||
### `--upper-time-bound`
|
||||
- **optional**: true
|
||||
- **type:** ISO 8601 datetime string
|
||||
- **default:** `None`
|
||||
- **description:** Only documents updated before this date will be indexed.
|
||||
|
||||
### `--async`
|
||||
- **type:** Boolean flag
|
||||
- **default:** `False`
|
||||
+- **description:** When set, dispatches the indexing job to a Celery worker instead of running it synchronously.
|
||||
|
||||
## Crash Safe Mode
|
||||
|
||||
The command saves the `updated_at` of the last document of each successful batch into the `bulk-indexer-checkpoint` cache variable.
|
||||
If the process crashes, this value can be used as `lower-time-bound` to resume from the last successfully indexed document.
|
||||
@@ -41,7 +41,8 @@ These are the environment variables you can set for the `impress-backend` contai
|
||||
| CONVERSION_FILE_MAX_SIZE | The file max size allowed when uploaded to convert it | 20971520 (20MB) |
|
||||
| CONVERSION_FILE_EXTENSIONS_ALLOWED | Extension list managed by the conversion service | [".docx", ".md"] |
|
||||
| CRISP_WEBSITE_ID | Crisp website id for support | |
|
||||
| DB_ENGINE | Engine to use for database connections | django.db.backends.postgresql_psycopg2 |
|
||||
| DB_CONN_MAX_AGE | Maximum lifetime of a database connection in seconds. Use 0 to close connections at the end of each request (default). When using the psycopg pool, set to 0 so connections are returned to the pool after each request. | 0 |
|
||||
| DB_ENGINE | Engine to use for database connections | django.db.backends.postgresql |
|
||||
| DB_HOST | Host of the database | localhost |
|
||||
| DB_NAME | Name of the database | impress |
|
||||
| DB_PASSWORD | Password to authenticate with | pass |
|
||||
|
||||
@@ -102,3 +102,5 @@ SEARCH_INDEXER_SECRET=find-api-key-for-docs-with-exactly-50-chars-length # Key
|
||||
INDEXING_URL=http://find:8000/api/v1.0/documents/index/
|
||||
SEARCH_URL=http://find:8000/api/v1.0/documents/search/
|
||||
SEARCH_INDEXER_QUERY_LIMIT=50
|
||||
|
||||
CONVERSION_UPLOAD_ENABLED=true
|
||||
|
||||
@@ -60,10 +60,13 @@
|
||||
"groupName": "ignored js dependencies",
|
||||
"matchManagers": ["npm"],
|
||||
"matchPackageNames": [
|
||||
"@react-pdf/renderer",
|
||||
"fetch-mock",
|
||||
"node",
|
||||
"node-fetch",
|
||||
"react-resizable-panels",
|
||||
"stylelint",
|
||||
"stylelint-config-standard",
|
||||
"workbox-webpack-plugin"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,54 +1,15 @@
|
||||
"""Admin classes and registrations for core app."""
|
||||
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.contrib.auth import admin as auth_admin
|
||||
from django.core.management import call_command
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import redirect, render
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from treebeard.admin import TreeAdmin
|
||||
|
||||
from core import models
|
||||
from core.forms import RunIndexingForm
|
||||
from core.tasks.user_reconciliation import user_reconciliation_csv_import_job
|
||||
|
||||
# Customize the default admin site's get_app_list method
|
||||
_original_get_app_list = admin.site.get_app_list
|
||||
|
||||
|
||||
def custom_get_app_list(_self, request, app_label=None):
|
||||
"""Add custom commands to the app list."""
|
||||
app_list = _original_get_app_list(request, app_label)
|
||||
|
||||
# Add Commands app with Run Indexing command
|
||||
commands_app = {
|
||||
"name": _("Commands"),
|
||||
"app_label": "commands",
|
||||
"app_url": "#",
|
||||
"has_module_perms": True,
|
||||
"models": [
|
||||
{
|
||||
"name": _("Run indexing"),
|
||||
"object_name": "RunIndexing",
|
||||
"admin_url": "/admin/run-indexing/",
|
||||
"view_only": False,
|
||||
"add_url": None,
|
||||
"change_url": None,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
app_list.append(commands_app)
|
||||
return app_list
|
||||
|
||||
|
||||
# Monkey-patch the admin site
|
||||
admin.site.get_app_list = lambda request, app_label=None: custom_get_app_list(
|
||||
admin.site, request, app_label
|
||||
)
|
||||
|
||||
|
||||
@admin.register(models.User)
|
||||
class UserAdmin(auth_admin.UserAdmin):
|
||||
@@ -266,39 +227,3 @@ class InvitationAdmin(admin.ModelAdmin):
|
||||
def save_model(self, request, obj, form, change):
|
||||
obj.issuer = request.user
|
||||
obj.save()
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def run_indexing_view(request: HttpRequest):
|
||||
"""Custom admin view for running indexing commands."""
|
||||
if request.method == "POST":
|
||||
form = RunIndexingForm(request.POST)
|
||||
if form.is_valid():
|
||||
lower_time_bound = form.cleaned_data.get("lower_time_bound")
|
||||
upper_time_bound = form.cleaned_data.get("upper_time_bound")
|
||||
call_command(
|
||||
"index",
|
||||
batch_size=form.cleaned_data["batch_size"],
|
||||
lower_time_bound=lower_time_bound.isoformat()
|
||||
if lower_time_bound
|
||||
else None,
|
||||
upper_time_bound=upper_time_bound.isoformat()
|
||||
if upper_time_bound
|
||||
else None,
|
||||
async_mode=True,
|
||||
)
|
||||
messages.success(request, _("Indexing triggered!"))
|
||||
return redirect("run_indexing")
|
||||
messages.error(request, _("Please correct the errors below."))
|
||||
else:
|
||||
form = RunIndexingForm()
|
||||
|
||||
return render(
|
||||
request=request,
|
||||
template_name="runindexing.html",
|
||||
context={
|
||||
**admin.site.each_context(request),
|
||||
"title": "Run Indexing Command",
|
||||
"form": form,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -674,21 +674,17 @@ class DocumentViewSet(
|
||||
|
||||
return drf.response.Response(serializer.data)
|
||||
|
||||
@transaction.atomic
|
||||
def perform_create(self, serializer):
|
||||
"""Set the current user as creator and owner of the newly created object."""
|
||||
|
||||
# locks the table to ensure safe concurrent access
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001
|
||||
"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 uploaded_file and not settings.CONVERSION_UPLOAD_ENABLED:
|
||||
raise drf.exceptions.ValidationError(
|
||||
{"file": ["file upload is not allowed"]}
|
||||
)
|
||||
|
||||
# If a file is uploaded, convert it to Yjs format and set as content
|
||||
if uploaded_file:
|
||||
try:
|
||||
@@ -702,15 +698,25 @@ class DocumentViewSet(
|
||||
)
|
||||
serializer.validated_data["content"] = converted_content
|
||||
serializer.validated_data["title"] = uploaded_file.name
|
||||
logger.info("conversion ended successfully")
|
||||
except ConversionError as err:
|
||||
logger.error("could not convert file content with error: %s", 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,
|
||||
)
|
||||
with transaction.atomic():
|
||||
# locks the table to ensure safe concurrent access
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001
|
||||
"IN SHARE ROW EXCLUSIVE MODE;"
|
||||
)
|
||||
|
||||
obj = models.Document.add_root(
|
||||
creator=self.request.user,
|
||||
**serializer.validated_data,
|
||||
)
|
||||
serializer.instance = obj
|
||||
models.DocumentAccess.objects.create(
|
||||
document=obj,
|
||||
@@ -827,6 +833,7 @@ class DocumentViewSet(
|
||||
|
||||
queryset = self.queryset.filter(path_list)
|
||||
queryset = queryset.filter(id__in=favorite_documents_ids)
|
||||
queryset = queryset.filter(ancestors_deleted_at__isnull=True)
|
||||
queryset = queryset.annotate_user_roles(user)
|
||||
queryset = queryset.annotate(
|
||||
is_favorite=db.Value(True, output_field=db.BooleanField())
|
||||
@@ -2669,6 +2676,7 @@ class ConfigView(drf.views.APIView):
|
||||
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY",
|
||||
"CONVERSION_FILE_EXTENSIONS_ALLOWED",
|
||||
"CONVERSION_FILE_MAX_SIZE",
|
||||
"CONVERSION_UPLOAD_ENABLED",
|
||||
"CRISP_WEBSITE_ID",
|
||||
"ENVIRONMENT",
|
||||
"FRONTEND_CSS_URL",
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.conf import settings
|
||||
from django.contrib.auth.hashers import make_password
|
||||
|
||||
import factory.fuzzy
|
||||
from factory import post_generation
|
||||
from faker import Faker
|
||||
|
||||
from core import models
|
||||
@@ -160,20 +159,6 @@ class DocumentFactory(factory.django.DjangoModelFactory):
|
||||
document=self, user=item, defaults={"is_masked": True}
|
||||
)
|
||||
|
||||
@post_generation
|
||||
def updated_at(self, create, extracted, **kwargs):
|
||||
"""
|
||||
the BaseModel.updated_at has auto_now=True.
|
||||
This prevents setting a specific updated_at value with the factory.
|
||||
|
||||
This post_generation method bypasses this behavior.
|
||||
"""
|
||||
if not create or not extracted:
|
||||
return
|
||||
|
||||
self.__class__.objects.filter(pk=self.pk).update(updated_at=extracted)
|
||||
self.refresh_from_db()
|
||||
|
||||
|
||||
class UserDocumentAccessFactory(factory.django.DjangoModelFactory):
|
||||
"""Create fake document user accesses for testing."""
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
"""Forms for the core app."""
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class RunIndexingForm(forms.Form):
|
||||
"""
|
||||
Form for running the indexing process.
|
||||
"""
|
||||
|
||||
batch_size = forms.IntegerField(
|
||||
min_value=1,
|
||||
initial=settings.SEARCH_INDEXER_BATCH_SIZE,
|
||||
)
|
||||
lower_time_bound = forms.DateTimeField(
|
||||
required=False, widget=forms.TextInput(attrs={"type": "datetime-local"})
|
||||
)
|
||||
upper_time_bound = forms.DateTimeField(
|
||||
required=False, widget=forms.TextInput(attrs={"type": "datetime-local"})
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
"""Override clean to validate time bounds."""
|
||||
cleaned_data = super().clean()
|
||||
self.check_time_bounds()
|
||||
return cleaned_data
|
||||
|
||||
def check_time_bounds(self):
|
||||
"""Validate that lower_time_bound is before upper_time_bound."""
|
||||
lower_time_bound = self.cleaned_data.get("lower_time_bound")
|
||||
upper_time_bound = self.cleaned_data.get("upper_time_bound")
|
||||
if (
|
||||
lower_time_bound
|
||||
and upper_time_bound
|
||||
and lower_time_bound > upper_time_bound
|
||||
):
|
||||
self.add_error(
|
||||
"upper_time_bound",
|
||||
_("Upper time bound must be after lower time bound."),
|
||||
)
|
||||
@@ -4,16 +4,12 @@ Handle search setup that needs to be done at bootstrap time.
|
||||
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from core import models
|
||||
from core.services.search_indexers import get_document_indexer
|
||||
from core.tasks.search import batch_document_indexer_task
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("docs.search.bootstrap_search")
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -28,32 +24,9 @@ class Command(BaseCommand):
|
||||
action="store",
|
||||
dest="batch_size",
|
||||
type=int,
|
||||
default=settings.SEARCH_INDEXER_BATCH_SIZE,
|
||||
default=50,
|
||||
help="Indexation query batch size",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lower-time-bound",
|
||||
action="store",
|
||||
dest="lower_time_bound",
|
||||
type=datetime.fromisoformat,
|
||||
default=None,
|
||||
help="DateTime in ISO format. Only documents updated after this date will be indexed",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--upper-time-bound",
|
||||
action="store",
|
||||
dest="upper_time_bound",
|
||||
type=datetime.fromisoformat,
|
||||
default=None,
|
||||
help="DateTime in ISO format. Only documents updated before this date will be indexed",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--async",
|
||||
action="store_true",
|
||||
dest="async_mode",
|
||||
default=False,
|
||||
help="Whether to execute indexing asynchronously in a Celery task (default: False)",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Launch and log search index generation."""
|
||||
@@ -62,38 +35,18 @@ class Command(BaseCommand):
|
||||
if not indexer:
|
||||
raise CommandError("The indexer is not enabled or properly configured.")
|
||||
|
||||
if options["async_mode"]:
|
||||
try:
|
||||
batch_document_indexer_task.apply_async(
|
||||
kwargs={
|
||||
"lower_time_bound": options["lower_time_bound"],
|
||||
"upper_time_bound": options["upper_time_bound"],
|
||||
"batch_size": options["batch_size"],
|
||||
"crash_safe_mode": True,
|
||||
},
|
||||
)
|
||||
except Exception as err:
|
||||
raise CommandError("Unable to dispatch indexing task") from err
|
||||
logger.info("Document indexing task sent to worker")
|
||||
else:
|
||||
logger.info("Starting to regenerate Find index...")
|
||||
start = time.perf_counter()
|
||||
logger.info("Starting to regenerate Find index...")
|
||||
start = time.perf_counter()
|
||||
batch_size = options["batch_size"]
|
||||
|
||||
try:
|
||||
count = indexer.index(
|
||||
queryset=models.Document.objects.filter_updated_at(
|
||||
lower_time_bound=options["lower_time_bound"],
|
||||
upper_time_bound=options["upper_time_bound"],
|
||||
),
|
||||
batch_size=options["batch_size"],
|
||||
crash_safe_mode=True,
|
||||
)
|
||||
except Exception as err:
|
||||
raise CommandError("Unable to regenerate index") from err
|
||||
try:
|
||||
count = indexer.index(batch_size=batch_size)
|
||||
except Exception as err:
|
||||
raise CommandError("Unable to regenerate index") from err
|
||||
|
||||
duration = time.perf_counter() - start
|
||||
logger.info(
|
||||
"Search index regenerated from %d document(s) in %.2f seconds.",
|
||||
count,
|
||||
duration,
|
||||
)
|
||||
duration = time.perf_counter() - start
|
||||
logger.info(
|
||||
"Search index regenerated from %d document(s) in %.2f seconds.",
|
||||
count,
|
||||
duration,
|
||||
)
|
||||
|
||||
@@ -267,6 +267,16 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
if settings.USER_ONBOARDING_SANDBOX_DOCUMENT:
|
||||
# transaction.atomic is used in a context manager to avoid a transaction if
|
||||
# the settings USER_ONBOARDING_SANDBOX_DOCUMENT is unused
|
||||
sandbox_id = settings.USER_ONBOARDING_SANDBOX_DOCUMENT
|
||||
try:
|
||||
template_document = Document.objects.get(id=sandbox_id)
|
||||
except Document.DoesNotExist:
|
||||
logger.warning(
|
||||
"Onboarding sandbox document with id %s does not exist. Skipping.",
|
||||
sandbox_id,
|
||||
)
|
||||
return
|
||||
|
||||
with transaction.atomic():
|
||||
# locks the table to ensure safe concurrent access
|
||||
with connection.cursor() as cursor:
|
||||
@@ -274,17 +284,6 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
f'LOCK TABLE "{Document._meta.db_table}" ' # noqa: SLF001
|
||||
"IN SHARE ROW EXCLUSIVE MODE;"
|
||||
)
|
||||
|
||||
sandbox_id = settings.USER_ONBOARDING_SANDBOX_DOCUMENT
|
||||
try:
|
||||
template_document = Document.objects.get(id=sandbox_id)
|
||||
except Document.DoesNotExist:
|
||||
logger.warning(
|
||||
"Onboarding sandbox document with id %s does not exist. Skipping.",
|
||||
sandbox_id,
|
||||
)
|
||||
return
|
||||
|
||||
sandbox_document = Document.add_root(
|
||||
title=template_document.title,
|
||||
content=template_document.content,
|
||||
@@ -859,32 +858,6 @@ class DocumentQuerySet(MP_NodeQuerySet):
|
||||
user_roles=models.Value([], output_field=output_field),
|
||||
)
|
||||
|
||||
def filter_updated_at(self, lower_time_bound=None, upper_time_bound=None):
|
||||
"""
|
||||
Filter documents by update_at.
|
||||
|
||||
Args:
|
||||
lower_time_bound (datetime, optional):
|
||||
Keep documents updated after this timestamp.
|
||||
upper_time_bound (datetime, optional):
|
||||
Keep documents updated before this timestamp.
|
||||
|
||||
Returns:
|
||||
QuerySet: Filtered queryset ready for indexation.
|
||||
"""
|
||||
conditions = models.Q()
|
||||
if lower_time_bound and upper_time_bound:
|
||||
conditions = models.Q(
|
||||
updated_at__gte=lower_time_bound,
|
||||
updated_at__lte=upper_time_bound,
|
||||
)
|
||||
elif lower_time_bound:
|
||||
conditions = models.Q(updated_at__gte=lower_time_bound)
|
||||
elif upper_time_bound:
|
||||
conditions = models.Q(updated_at__lte=upper_time_bound)
|
||||
|
||||
return self.filter(conditions)
|
||||
|
||||
|
||||
class DocumentManager(MP_NodeManager.from_queryset(DocumentQuerySet)):
|
||||
"""
|
||||
@@ -1485,16 +1458,13 @@ class Document(MP_Node, BaseModel):
|
||||
.first()
|
||||
)
|
||||
self.ancestors_deleted_at = ancestors_deleted_at
|
||||
self.save(update_fields=["deleted_at", "ancestors_deleted_at", "updated_at"])
|
||||
self.save(update_fields=["deleted_at", "ancestors_deleted_at"])
|
||||
self.invalidate_nb_accesses_cache()
|
||||
|
||||
self.get_descendants().exclude(
|
||||
models.Q(deleted_at__isnull=False)
|
||||
| models.Q(ancestors_deleted_at__lt=current_deleted_at)
|
||||
).update(
|
||||
ancestors_deleted_at=self.ancestors_deleted_at,
|
||||
updated_at=self.updated_at,
|
||||
)
|
||||
).update(ancestors_deleted_at=self.ancestors_deleted_at)
|
||||
|
||||
if self.depth > 1:
|
||||
self._meta.model.objects.filter(pk=self.get_parent().pk).update(
|
||||
|
||||
@@ -45,6 +45,8 @@ class Converter:
|
||||
def convert(self, data, content_type, accept):
|
||||
"""Convert input into other formats using external microservices."""
|
||||
|
||||
logger.info("converting content from %s to %s", content_type, accept)
|
||||
|
||||
if content_type == mime_types.DOCX and accept == mime_types.YJS:
|
||||
blocknote_data = self.docspec.convert(
|
||||
data, mime_types.DOCX, mime_types.BLOCKNOTE
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Document search index management utilities and indexers"""
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
@@ -126,44 +125,44 @@ class BaseDocumentIndexer(ABC):
|
||||
if not self.search_url:
|
||||
raise ImproperlyConfigured("SEARCH_URL must be set in Django settings.")
|
||||
|
||||
def index(self, queryset, batch_size=None, crash_safe_mode=False):
|
||||
def index(self, queryset=None, batch_size=None):
|
||||
"""
|
||||
Fetch documents in batches, serialize them, and push to the search backend.
|
||||
|
||||
Args:
|
||||
queryset: Document queryset
|
||||
queryset (optional): Document queryset
|
||||
Defaults to all documents without filter.
|
||||
batch_size (int, optional): Number of documents per batch.
|
||||
Defaults to settings.SEARCH_INDEXER_BATCH_SIZE.
|
||||
crash_safe_mode (bool, optional): If True, order documents by updated_at
|
||||
This allows resuming indexing from the last successful batch in case of a crash
|
||||
but is more computationally expensive due to sorting.
|
||||
"""
|
||||
last_id = 0
|
||||
count = 0
|
||||
queryset = queryset or models.Document.objects.all()
|
||||
batch_size = batch_size or self.batch_size
|
||||
|
||||
if crash_safe_mode:
|
||||
queryset = queryset.order_by("updated_at")
|
||||
while True:
|
||||
documents_batch = list(
|
||||
queryset.filter(
|
||||
id__gt=last_id,
|
||||
).order_by("id")[:batch_size]
|
||||
)
|
||||
|
||||
if not documents_batch:
|
||||
break
|
||||
|
||||
for documents_batch in itertools.batched(queryset.iterator(), batch_size):
|
||||
doc_paths = [doc.path for doc in documents_batch]
|
||||
last_id = documents_batch[-1].id
|
||||
accesses_by_document_path = get_batch_accesses_by_users_and_teams(doc_paths)
|
||||
|
||||
serialized_batch = [
|
||||
self.serialize_document(document, accesses_by_document_path)
|
||||
for document in documents_batch
|
||||
if document.content or document.title
|
||||
]
|
||||
|
||||
if not serialized_batch:
|
||||
continue
|
||||
|
||||
self.push(serialized_batch)
|
||||
count += len(serialized_batch)
|
||||
|
||||
if crash_safe_mode:
|
||||
logger.info(
|
||||
"Indexing checkpoint: %s.",
|
||||
serialized_batch[-1]["updated_at"],
|
||||
)
|
||||
if serialized_batch:
|
||||
self.push(serialized_batch)
|
||||
count += len(serialized_batch)
|
||||
|
||||
return count
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from logging import getLogger
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Q
|
||||
|
||||
from django_redis.cache import RedisCache
|
||||
|
||||
@@ -19,12 +20,7 @@ logger = getLogger(__file__)
|
||||
|
||||
@app.task
|
||||
def document_indexer_task(document_id):
|
||||
"""
|
||||
Celery Task: Indexes a single document by its ID.
|
||||
|
||||
Args:
|
||||
document_id: Primary key of the document to index.
|
||||
"""
|
||||
"""Celery Task : Sends indexation query for a document."""
|
||||
indexer = get_document_indexer()
|
||||
|
||||
if indexer:
|
||||
@@ -34,17 +30,8 @@ def document_indexer_task(document_id):
|
||||
|
||||
def batch_indexer_throttle_acquire(timeout: int = 0, atomic: bool = True):
|
||||
"""
|
||||
Acquire a throttle lock to prevent multiple batch indexation tasks during countdown.
|
||||
|
||||
implements a debouncing pattern: only the first call during the timeout period
|
||||
will succeed, subsequent calls are skipped until the timeout expires.
|
||||
|
||||
Args:
|
||||
timeout (int): Lock duration in seconds (countdown period).
|
||||
atomic (bool): Use Redis locks for atomic operations if available.
|
||||
|
||||
Returns:
|
||||
bool: True if lock acquired (first call), False if already held (subsequent calls).
|
||||
Enable the task throttle flag for a delay.
|
||||
Uses redis locks if available to ensure atomic changes
|
||||
"""
|
||||
key = "document-batch-indexer-throttle"
|
||||
|
||||
@@ -54,65 +41,44 @@ def batch_indexer_throttle_acquire(timeout: int = 0, atomic: bool = True):
|
||||
with cache.locks(key):
|
||||
return batch_indexer_throttle_acquire(timeout, atomic=False)
|
||||
|
||||
# cache.add() is atomic test-and-set operation:
|
||||
# - If key doesn't exist: creates it with timeout and returns True
|
||||
# - If key already exists: does nothing and returns False
|
||||
# The key expires after timeout seconds, releasing the lock.
|
||||
# The value 1 is irrelevant, only the key presence/absence matters.
|
||||
# Use add() here :
|
||||
# - set the flag and returns true if not exist
|
||||
# - do nothing and return false if exist
|
||||
return cache.add(key, 1, timeout=timeout)
|
||||
|
||||
|
||||
@app.task
|
||||
def batch_document_indexer_task(lower_time_bound=None, upper_time_bound=None, **kwargs):
|
||||
"""
|
||||
Celery Task: Batch indexes all documents modified since timestamp.
|
||||
|
||||
Args:
|
||||
lower_time_bound (datetime, optional):
|
||||
indexes documents updated or deleted after this timestamp.
|
||||
upper_time_bound (datetime, optional):
|
||||
indexes documents updated or deleted before this timestamp.
|
||||
"""
|
||||
def batch_document_indexer_task(timestamp):
|
||||
"""Celery Task : Sends indexation query for a batch of documents."""
|
||||
indexer = get_document_indexer()
|
||||
|
||||
if not indexer:
|
||||
logger.warning("Indexing task triggered but no indexer configured: skipping")
|
||||
return
|
||||
if indexer:
|
||||
queryset = models.Document.objects.filter(
|
||||
Q(updated_at__gte=timestamp)
|
||||
| Q(deleted_at__gte=timestamp)
|
||||
| Q(ancestors_deleted_at__gte=timestamp)
|
||||
)
|
||||
|
||||
count = indexer.index(
|
||||
queryset=models.Document.objects.filter_updated_at(
|
||||
lower_time_bound=lower_time_bound, upper_time_bound=upper_time_bound
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
logger.info("Indexed %d documents", count)
|
||||
count = indexer.index(queryset)
|
||||
logger.info("Indexed %d documents", count)
|
||||
|
||||
|
||||
def trigger_batch_document_indexer(document):
|
||||
"""
|
||||
Trigger document indexation with optional debounce mechanism.
|
||||
|
||||
behavior depends on SEARCH_INDEXER_COUNTDOWN setting:
|
||||
- if countdown > 0 sec (async batch mode):
|
||||
* schedules a batch indexation task after countdown in seconds
|
||||
* uses throttle mechanism to ensure only ONE batch task runs per countdown period
|
||||
* all documents modified since first trigger are indexed together
|
||||
- if countdown == 0 sec (sync mode):
|
||||
* executes indexation synchronously in the current thread
|
||||
* no batching, no throttling, no Celery task queuing
|
||||
Trigger indexation task with debounce a delay set by the SEARCH_INDEXER_COUNTDOWN setting.
|
||||
|
||||
Args:
|
||||
document (Document): the document instance that triggered the indexation.
|
||||
document (Document): The document instance.
|
||||
"""
|
||||
countdown = int(settings.SEARCH_INDEXER_COUNTDOWN)
|
||||
|
||||
# DO NOT create a task if indexation is disabled
|
||||
# DO NOT create a task if indexation if disabled
|
||||
if not settings.SEARCH_INDEXER_CLASS:
|
||||
return
|
||||
|
||||
if countdown > 0:
|
||||
# use throttle to ensure only one task is scheduled per countdown period.
|
||||
# if throttle acquired, schedule batch task; otherwise skip.
|
||||
# Each time this method is called during a countdown, we increment the
|
||||
# counter and each task decrease it, so the index be run only once.
|
||||
if batch_indexer_throttle_acquire(timeout=countdown):
|
||||
logger.info(
|
||||
"Add task for batch document indexation from updated_at=%s in %d seconds",
|
||||
@@ -121,7 +87,7 @@ def trigger_batch_document_indexer(document):
|
||||
)
|
||||
|
||||
batch_document_indexer_task.apply_async(
|
||||
kwargs={"lower_time_bound": document.updated_at}, countdown=countdown
|
||||
args=[document.updated_at], countdown=countdown
|
||||
)
|
||||
else:
|
||||
logger.info("Skip task for batch document %s indexation", document.pk)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<form method="POST" >
|
||||
{% csrf_token %}
|
||||
|
||||
<hr style="margin-bottom: 10px;">
|
||||
|
||||
<p>
|
||||
{% translate "This command triggers the indexing of all documents within the specified time bound." %}
|
||||
</p>
|
||||
|
||||
<hr style="margin-bottom: 20px;">
|
||||
|
||||
{{ form.as_p }}
|
||||
|
||||
<input type="submit" value="{% translate 'Run Indexing' %}" style="margin-top: 20px;">
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -2,25 +2,21 @@
|
||||
Unit test for `index` command.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from operator import itemgetter
|
||||
from unittest import mock
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.core.management import CommandError, call_command
|
||||
from django.db import transaction
|
||||
|
||||
import pytest
|
||||
|
||||
from core import factories
|
||||
from core.factories import DocumentFactory
|
||||
from core.services.search_indexers import FindDocumentIndexer
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
def test_index_without_bound_success():
|
||||
def test_index():
|
||||
"""Test the command `index` that run the Find app indexer for all the available documents."""
|
||||
user = factories.UserFactory()
|
||||
indexer = FindDocumentIndexer()
|
||||
@@ -43,152 +39,18 @@ def test_index_without_bound_success():
|
||||
with mock.patch.object(FindDocumentIndexer, "push") as mock_push:
|
||||
call_command("index")
|
||||
|
||||
push_call_args = [call.args[0] for call in mock_push.call_args_list]
|
||||
push_call_args = [call.args[0] for call in mock_push.call_args_list]
|
||||
|
||||
# called once but with a batch of docs
|
||||
mock_push.assert_called_once()
|
||||
# called once but with a batch of docs
|
||||
mock_push.assert_called_once()
|
||||
|
||||
assert sorted(push_call_args[0], key=itemgetter("id")) == sorted(
|
||||
[
|
||||
indexer.serialize_document(doc, accesses),
|
||||
indexer.serialize_document(no_title_doc, accesses),
|
||||
],
|
||||
key=itemgetter("id"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
def test_index_with_both_bounds_success():
|
||||
"""Test the command `index` for all documents within time bound."""
|
||||
cache.clear()
|
||||
lower_time_bound = datetime(2024, 2, 1, tzinfo=timezone.utc)
|
||||
upper_time_bound = lower_time_bound + timedelta(days=30)
|
||||
|
||||
document_too_early = DocumentFactory(
|
||||
updated_at=lower_time_bound - timedelta(days=10)
|
||||
)
|
||||
document_in_window_1 = DocumentFactory(
|
||||
updated_at=lower_time_bound + timedelta(days=5)
|
||||
)
|
||||
document_in_window_2 = DocumentFactory(
|
||||
updated_at=lower_time_bound + timedelta(days=15)
|
||||
)
|
||||
document_too_late = DocumentFactory(
|
||||
updated_at=upper_time_bound + timedelta(days=10)
|
||||
)
|
||||
|
||||
with mock.patch.object(FindDocumentIndexer, "push") as mock_push:
|
||||
call_command(
|
||||
"index",
|
||||
lower_time_bound=lower_time_bound.isoformat(),
|
||||
upper_time_bound=upper_time_bound.isoformat(),
|
||||
assert sorted(push_call_args[0], key=itemgetter("id")) == sorted(
|
||||
[
|
||||
indexer.serialize_document(doc, accesses),
|
||||
indexer.serialize_document(no_title_doc, accesses),
|
||||
],
|
||||
key=itemgetter("id"),
|
||||
)
|
||||
pushed_document_ids = [
|
||||
document["id"]
|
||||
for call_arg_list in mock_push.call_args_list
|
||||
for document in call_arg_list.args[0]
|
||||
]
|
||||
|
||||
# Only documents in window should be indexed
|
||||
assert str(document_too_early.id) not in pushed_document_ids
|
||||
assert str(document_in_window_1.id) in pushed_document_ids
|
||||
assert str(document_in_window_2.id) in pushed_document_ids
|
||||
assert str(document_too_late.id) not in pushed_document_ids
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
def test_index_with_crash_recovery(caplog_with_propagate):
|
||||
"""Test resuming indexing from checkpoint after a crash."""
|
||||
cache.clear()
|
||||
lower_time_bound = datetime(2024, 2, 1, tzinfo=timezone.utc)
|
||||
upper_time_bound = lower_time_bound + timedelta(days=60)
|
||||
|
||||
batch_size = 2
|
||||
documents = [
|
||||
# batch 0
|
||||
factories.DocumentFactory(updated_at=lower_time_bound + timedelta(days=5)),
|
||||
factories.DocumentFactory(updated_at=lower_time_bound + timedelta(days=10)),
|
||||
# batch 1
|
||||
factories.DocumentFactory(updated_at=lower_time_bound + timedelta(days=20)),
|
||||
factories.DocumentFactory(updated_at=lower_time_bound + timedelta(days=25)),
|
||||
# batch 2 - will crash here
|
||||
factories.DocumentFactory(updated_at=lower_time_bound + timedelta(days=30)),
|
||||
factories.DocumentFactory(updated_at=lower_time_bound + timedelta(days=35)),
|
||||
# batch 3
|
||||
factories.DocumentFactory(updated_at=lower_time_bound + timedelta(days=40)),
|
||||
factories.DocumentFactory(updated_at=lower_time_bound + timedelta(days=45)),
|
||||
]
|
||||
|
||||
def push_with_failure_on_batch_2(data):
|
||||
# Crash when encounters document at index 4 (batch 2 with batch_size=2)
|
||||
if str(documents[4].id) in [document["id"] for document in data]:
|
||||
raise ConnectionError("Simulated indexing error")
|
||||
|
||||
# First run: simulate crash on batch 3
|
||||
with mock.patch.object(FindDocumentIndexer, "push") as mock_push:
|
||||
mock_push.side_effect = push_with_failure_on_batch_2
|
||||
with pytest.raises(CommandError):
|
||||
with caplog_with_propagate.at_level(logging.INFO):
|
||||
call_command(
|
||||
"index",
|
||||
batch_size=batch_size,
|
||||
lower_time_bound=lower_time_bound.isoformat(),
|
||||
upper_time_bound=upper_time_bound.isoformat(),
|
||||
)
|
||||
pushed_document_ids = [
|
||||
document["id"]
|
||||
for call_arg_list in mock_push.call_args_list
|
||||
for document in call_arg_list.args[0]
|
||||
]
|
||||
|
||||
# the updated at of the last document of each batch are logged as checkpoint
|
||||
# -> documents[3].updated_at is the most advanced checkpoint
|
||||
for i in [1, 3]:
|
||||
assert any(
|
||||
f"Indexing checkpoint: {documents[i].updated_at.isoformat()}." in message
|
||||
for message in caplog_with_propagate.messages
|
||||
)
|
||||
for i in [0, 2, 4, 5, 6]:
|
||||
assert not any(
|
||||
f"Indexing checkpoint: {documents[i].updated_at.isoformat()}" in message
|
||||
for message in caplog_with_propagate.messages
|
||||
)
|
||||
# first 2 batches should be indexed successfully
|
||||
for i in range(0, 4):
|
||||
assert str(documents[i].id) in pushed_document_ids
|
||||
# next batch should have been attempted but failed
|
||||
for i in range(4, 6):
|
||||
assert str(documents[i].id) in pushed_document_ids
|
||||
# last batches indexing should not have been attempted
|
||||
for i in range(6, 8):
|
||||
assert str(documents[i].id) not in pushed_document_ids
|
||||
|
||||
# Second run: resume from checkpoint
|
||||
with mock.patch.object(FindDocumentIndexer, "push") as mock_push:
|
||||
call_command(
|
||||
"index",
|
||||
batch_size=batch_size,
|
||||
lower_time_bound=documents[3].updated_at,
|
||||
upper_time_bound=upper_time_bound.isoformat(),
|
||||
)
|
||||
pushed_document_ids = [
|
||||
document["id"]
|
||||
for call_arg_list in mock_push.call_args_list
|
||||
for document in call_arg_list.args[0]
|
||||
]
|
||||
|
||||
# first 2 batches should NOT be re-indexed
|
||||
# except the last document of the last batch which is on the checkpoint boundary
|
||||
# -> doc 0, 1 and 2
|
||||
for i in range(0, 3):
|
||||
assert str(documents[i].id) not in pushed_document_ids
|
||||
# next batches should be indexed including the document at the checkpoint boundary
|
||||
# which has already been indexed and is re-indexed
|
||||
# -> doc 3 to the end
|
||||
for i in range(3, 8):
|
||||
assert str(documents[i].id) in pushed_document_ids
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -201,57 +63,3 @@ def test_index_improperly_configured(indexer_settings):
|
||||
call_command("index")
|
||||
|
||||
assert str(err.value) == "The indexer is not enabled or properly configured."
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
def test_index_with_async_flag(settings):
|
||||
"""Test the command `index` with --async=True runs task asynchronously."""
|
||||
cache.clear()
|
||||
lower_time_bound = datetime(2024, 2, 1, tzinfo=timezone.utc)
|
||||
|
||||
with mock.patch(
|
||||
"core.management.commands.index.batch_document_indexer_task"
|
||||
) as mock_task:
|
||||
with mock.patch.object(FindDocumentIndexer, "push") as mock_push:
|
||||
call_command(
|
||||
"index", async_mode=True, lower_time_bound=lower_time_bound.isoformat()
|
||||
)
|
||||
# push not be called synchronously
|
||||
mock_push.assert_not_called()
|
||||
# task called asynchronously
|
||||
mock_task.apply_async.assert_called_once_with(
|
||||
kwargs={
|
||||
"lower_time_bound": lower_time_bound.isoformat(),
|
||||
"upper_time_bound": None,
|
||||
"batch_size": settings.SEARCH_INDEXER_BATCH_SIZE,
|
||||
"crash_safe_mode": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
def test_index_without_async_flag():
|
||||
"""Test the command `index` with --async=False runs synchronously."""
|
||||
cache.clear()
|
||||
lower_time_bound = datetime(2024, 2, 1, tzinfo=timezone.utc)
|
||||
|
||||
document = DocumentFactory(updated_at=lower_time_bound + timedelta(days=10))
|
||||
|
||||
with mock.patch(
|
||||
"core.management.commands.index.batch_document_indexer_task"
|
||||
) as mock_task:
|
||||
with mock.patch.object(FindDocumentIndexer, "push") as mock_push:
|
||||
call_command(
|
||||
"index", async_mode=False, lower_time_bound=lower_time_bound.isoformat()
|
||||
)
|
||||
# push is called synchronously to index the document
|
||||
pushed_document_ids = [
|
||||
document["id"]
|
||||
for call_arg_list in mock_push.call_args_list
|
||||
for document in call_arg_list.args[0]
|
||||
]
|
||||
assert str(document.id) in pushed_document_ids
|
||||
# async task not called
|
||||
mock_task.apply_async.assert_not_called()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Fixtures for tests in the impress core application"""
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from unittest import mock
|
||||
|
||||
from django.core.cache import cache
|
||||
@@ -23,30 +22,6 @@ def clear_cache():
|
||||
cache.clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def caplog_with_propagate(settings, caplog):
|
||||
"""
|
||||
propagate=False on settings.LOGGING loggers.
|
||||
This prevents caplog from capturing logs.
|
||||
|
||||
This fixture enables propagation on all configured loggers.
|
||||
"""
|
||||
# Save original propagate state
|
||||
original_propagate = {}
|
||||
|
||||
for logger_name in settings.LOGGING.get("loggers", {}):
|
||||
logger = logging.getLogger(logger_name)
|
||||
original_propagate[logger_name] = logger.propagate
|
||||
logger.propagate = True
|
||||
|
||||
try:
|
||||
yield caplog
|
||||
finally:
|
||||
# Restore original propagate states
|
||||
for logger_name, original_value in original_propagate.items():
|
||||
logging.getLogger(logger_name).propagate = original_value
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_user_teams():
|
||||
"""Mock for the "teams" property on the User model."""
|
||||
|
||||
@@ -40,7 +40,7 @@ def test_api_documents_create_with_file_anonymous():
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_docx_file_success(mock_convert):
|
||||
def test_api_documents_create_with_docx_file_success(mock_convert, settings):
|
||||
"""
|
||||
Authenticated users should be able to create documents by uploading a DOCX file.
|
||||
The file should be converted to YJS format and the title should be set from filename.
|
||||
@@ -49,6 +49,8 @@ def test_api_documents_create_with_docx_file_success(mock_convert):
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
settings.CONVERSION_UPLOAD_ENABLED = True
|
||||
|
||||
# Mock the conversion
|
||||
converted_yjs = "base64encodedyjscontent"
|
||||
mock_convert.return_value = converted_yjs
|
||||
@@ -81,7 +83,38 @@ def test_api_documents_create_with_docx_file_success(mock_convert):
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_markdown_file_success(mock_convert):
|
||||
def test_api_documents_create_with_docx_file_disabled(mock_convert, settings):
|
||||
"""
|
||||
When conversion is not enabled, uploading a file should have no effect
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
settings.CONVERSION_UPLOAD_ENABLED = False
|
||||
|
||||
# Create a fake DOCX file
|
||||
file_content = b"fake docx content"
|
||||
file = BytesIO(file_content)
|
||||
file.name = "My Important Document.docx"
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/documents/",
|
||||
{
|
||||
"file": file,
|
||||
},
|
||||
format="multipart",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"file": ["file upload is not allowed"]}
|
||||
|
||||
# Verify the converter was not called
|
||||
mock_convert.assert_not_called()
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_markdown_file_success(mock_convert, settings):
|
||||
"""
|
||||
Authenticated users should be able to create documents by uploading a Markdown file.
|
||||
"""
|
||||
@@ -89,6 +122,8 @@ def test_api_documents_create_with_markdown_file_success(mock_convert):
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
settings.CONVERSION_UPLOAD_ENABLED = True
|
||||
|
||||
# Mock the conversion
|
||||
converted_yjs = "base64encodedyjscontent"
|
||||
mock_convert.return_value = converted_yjs
|
||||
@@ -121,7 +156,7 @@ def test_api_documents_create_with_markdown_file_success(mock_convert):
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_file_and_explicit_title(mock_convert):
|
||||
def test_api_documents_create_with_file_and_explicit_title(mock_convert, settings):
|
||||
"""
|
||||
When both file and title are provided, the filename should override the title.
|
||||
"""
|
||||
@@ -129,6 +164,8 @@ def test_api_documents_create_with_file_and_explicit_title(mock_convert):
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
settings.CONVERSION_UPLOAD_ENABLED = True
|
||||
|
||||
# Mock the conversion
|
||||
converted_yjs = "base64encodedyjscontent"
|
||||
mock_convert.return_value = converted_yjs
|
||||
@@ -153,7 +190,7 @@ def test_api_documents_create_with_file_and_explicit_title(mock_convert):
|
||||
assert document.title == "Uploaded Document.docx"
|
||||
|
||||
|
||||
def test_api_documents_create_with_empty_file():
|
||||
def test_api_documents_create_with_empty_file(settings):
|
||||
"""
|
||||
Creating a document with an empty file should fail with a validation error.
|
||||
"""
|
||||
@@ -161,6 +198,8 @@ def test_api_documents_create_with_empty_file():
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
settings.CONVERSION_UPLOAD_ENABLED = True
|
||||
|
||||
# Create an empty file
|
||||
file = BytesIO(b"")
|
||||
file.name = "empty.docx"
|
||||
@@ -179,7 +218,7 @@ def test_api_documents_create_with_empty_file():
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_file_conversion_error(mock_convert):
|
||||
def test_api_documents_create_with_file_conversion_error(mock_convert, settings):
|
||||
"""
|
||||
When conversion fails, the API should return a 400 error with appropriate message.
|
||||
"""
|
||||
@@ -187,6 +226,8 @@ def test_api_documents_create_with_file_conversion_error(mock_convert):
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
settings.CONVERSION_UPLOAD_ENABLED = True
|
||||
|
||||
# Mock the conversion to raise an error
|
||||
mock_convert.side_effect = ConversionError("Failed to convert document")
|
||||
|
||||
@@ -209,7 +250,7 @@ def test_api_documents_create_with_file_conversion_error(mock_convert):
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_file_service_unavailable(mock_convert):
|
||||
def test_api_documents_create_with_file_service_unavailable(mock_convert, settings):
|
||||
"""
|
||||
When the conversion service is unavailable, appropriate error should be returned.
|
||||
"""
|
||||
@@ -217,6 +258,8 @@ def test_api_documents_create_with_file_service_unavailable(mock_convert):
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
settings.CONVERSION_UPLOAD_ENABLED = True
|
||||
|
||||
# Mock the conversion to raise ServiceUnavailableError
|
||||
mock_convert.side_effect = ServiceUnavailableError(
|
||||
"Failed to connect to conversion service"
|
||||
@@ -264,7 +307,7 @@ def test_api_documents_create_without_file_still_works():
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_file_null_value(mock_convert):
|
||||
def test_api_documents_create_with_file_null_value(mock_convert, settings):
|
||||
"""
|
||||
Passing file=null should be treated as no file upload.
|
||||
"""
|
||||
@@ -272,6 +315,8 @@ def test_api_documents_create_with_file_null_value(mock_convert):
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
settings.CONVERSION_UPLOAD_ENABLED = True
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/documents/",
|
||||
{
|
||||
@@ -289,7 +334,9 @@ def test_api_documents_create_with_file_null_value(mock_convert):
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_file_preserves_content_format(mock_convert):
|
||||
def test_api_documents_create_with_file_preserves_content_format(
|
||||
mock_convert, settings
|
||||
):
|
||||
"""
|
||||
Verify that the converted content is stored correctly in the document.
|
||||
"""
|
||||
@@ -297,6 +344,8 @@ def test_api_documents_create_with_file_preserves_content_format(mock_convert):
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
settings.CONVERSION_UPLOAD_ENABLED = True
|
||||
|
||||
# Mock the conversion with realistic base64-encoded YJS data
|
||||
converted_yjs = "AQMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fICA="
|
||||
mock_convert.return_value = converted_yjs
|
||||
@@ -328,7 +377,7 @@ def test_api_documents_create_with_file_preserves_content_format(mock_convert):
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_file_unicode_filename(mock_convert):
|
||||
def test_api_documents_create_with_file_unicode_filename(mock_convert, settings):
|
||||
"""
|
||||
Test that Unicode characters in filenames are handled correctly.
|
||||
"""
|
||||
@@ -336,6 +385,8 @@ def test_api_documents_create_with_file_unicode_filename(mock_convert):
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
settings.CONVERSION_UPLOAD_ENABLED = True
|
||||
|
||||
# Mock the conversion
|
||||
converted_yjs = "base64encodedyjscontent"
|
||||
mock_convert.return_value = converted_yjs
|
||||
@@ -363,6 +414,7 @@ def test_api_documents_create_with_file_max_size_exceeded(settings):
|
||||
The uploaded file should not exceed the maximum size in settings.
|
||||
"""
|
||||
settings.CONVERSION_FILE_MAX_SIZE = 1 # 1 byte for test
|
||||
settings.CONVERSION_UPLOAD_ENABLED = True
|
||||
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
@@ -389,6 +441,7 @@ def test_api_documents_create_with_file_extension_not_allowed(settings):
|
||||
The uploaded file should not have an allowed extension.
|
||||
"""
|
||||
settings.CONVERSION_FILE_EXTENSIONS_ALLOWED = [".docx"]
|
||||
settings.CONVERSION_UPLOAD_ENABLED = True
|
||||
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
|
||||
@@ -96,9 +96,8 @@ def test_api_documents_delete_authenticated_owner_of_ancestor(depth):
|
||||
)
|
||||
assert models.Document.objects.count() == depth
|
||||
|
||||
document_to_delete = documents[-1]
|
||||
response = client.delete(
|
||||
f"/api/v1.0/documents/{document_to_delete.id}/",
|
||||
f"/api/v1.0/documents/{documents[-1].id}/",
|
||||
)
|
||||
|
||||
assert response.status_code == 204
|
||||
@@ -106,11 +105,7 @@ def test_api_documents_delete_authenticated_owner_of_ancestor(depth):
|
||||
# Make sure it is only a soft delete
|
||||
assert models.Document.objects.count() == depth
|
||||
assert models.Document.objects.filter(deleted_at__isnull=True).count() == depth - 1
|
||||
deleted_documents = models.Document.objects.filter(deleted_at__isnull=False)
|
||||
assert len(deleted_documents) == 1
|
||||
deleted_document = deleted_documents[0]
|
||||
# updated_at is updated by the soft delete
|
||||
assert deleted_document.updated_at > document_to_delete.updated_at
|
||||
assert models.Document.objects.filter(deleted_at__isnull=False).count() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
|
||||
@@ -114,3 +114,29 @@ def test_api_document_favorite_list_with_favorite_children():
|
||||
assert content[0]["id"] == str(children[0].id)
|
||||
assert content[1]["id"] == str(children[1].id)
|
||||
assert content[2]["id"] == str(access.document.id)
|
||||
|
||||
|
||||
def test_api_document_favorite_list_with_deleted_child():
|
||||
"""
|
||||
Authenticated users should not see deleted documents in their favorite list.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
root = factories.DocumentFactory(creator=user, users=[user], favorited_by=[user])
|
||||
child1, child2 = factories.DocumentFactory.create_batch(
|
||||
2, parent=root, favorited_by=[user]
|
||||
)
|
||||
|
||||
child1.delete()
|
||||
|
||||
response = client.get("/api/v1.0/documents/favorite_list/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["count"] == 2
|
||||
|
||||
content = response.json()["results"]
|
||||
|
||||
assert content[0]["id"] == str(root.id)
|
||||
assert content[1]["id"] == str(child2.id)
|
||||
|
||||
@@ -91,15 +91,11 @@ def test_api_documents_restore_authenticated_owner_ancestor_deleted():
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
|
||||
|
||||
document.soft_delete()
|
||||
document.refresh_from_db()
|
||||
document_deleted_at = document.deleted_at
|
||||
document_updated_at = document.updated_at
|
||||
|
||||
assert document_deleted_at is not None
|
||||
|
||||
grand_parent.soft_delete()
|
||||
grand_parent_deleted_at = grand_parent.deleted_at
|
||||
|
||||
assert grand_parent_deleted_at is not None
|
||||
|
||||
response = client.post(f"/api/v1.0/documents/{document.id!s}/restore/")
|
||||
@@ -109,8 +105,6 @@ def test_api_documents_restore_authenticated_owner_ancestor_deleted():
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.deleted_at is None
|
||||
# document is updated by restore
|
||||
assert document.updated_at > document_updated_at
|
||||
# document is still marked as deleted
|
||||
assert document.ancestors_deleted_at == grand_parent_deleted_at
|
||||
assert grand_parent_deleted_at > document_deleted_at
|
||||
|
||||
@@ -261,7 +261,7 @@ def test_external_api_documents_create_subdocument_reader_not_allowed(
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_external_api_documents_create_with_markdown_file_success(
|
||||
mock_convert, user_token, resource_server_backend, user_specific_sub
|
||||
mock_convert, user_token, resource_server_backend, user_specific_sub, settings
|
||||
):
|
||||
"""
|
||||
Users with an access token should be able to create documents through the resource
|
||||
@@ -272,6 +272,8 @@ def test_external_api_documents_create_with_markdown_file_success(
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {user_token}")
|
||||
|
||||
settings.CONVERSION_UPLOAD_ENABLED = True
|
||||
|
||||
# Mock the conversion
|
||||
converted_yjs = "base64encodedyjscontent"
|
||||
mock_convert.return_value = converted_yjs
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
"""Tests for run_indexing_view admin endpoint."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.http import HttpResponse
|
||||
|
||||
import pytest
|
||||
|
||||
from core import factories
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"is_authenticated,is_staff,should_call_command",
|
||||
[
|
||||
(False, False, False),
|
||||
(True, False, False),
|
||||
(True, True, True),
|
||||
],
|
||||
)
|
||||
def test_run_indexing_view_post_authentication(
|
||||
client,
|
||||
is_authenticated,
|
||||
is_staff,
|
||||
should_call_command,
|
||||
):
|
||||
"""Test that POST to run_indexing_view requires staff authentication."""
|
||||
|
||||
if is_authenticated:
|
||||
user = factories.UserFactory(is_staff=is_staff)
|
||||
client.force_login(user)
|
||||
|
||||
batch_size = 100
|
||||
with patch("core.admin.call_command") as mock_call_command:
|
||||
mock_call_command.return_value = HttpResponse("Mocked render")
|
||||
response = client.post("/admin/run-indexing/", {"batch_size": batch_size})
|
||||
|
||||
# redirects in all cases
|
||||
assert response.status_code == 302
|
||||
|
||||
if should_call_command:
|
||||
assert "/admin/run-indexing/" == response.url
|
||||
mock_call_command.assert_called_once()
|
||||
assert mock_call_command.call_args.kwargs == {
|
||||
"batch_size": batch_size,
|
||||
"lower_time_bound": None,
|
||||
"upper_time_bound": None,
|
||||
"async_mode": True,
|
||||
}
|
||||
|
||||
else:
|
||||
assert "/admin/login/" in response.url
|
||||
mock_call_command.assert_not_called()
|
||||
@@ -26,6 +26,7 @@ pytestmark = pytest.mark.django_db
|
||||
API_USERS_SEARCH_QUERY_MIN_LENGTH=6,
|
||||
COLLABORATION_WS_URL="http://testcollab/",
|
||||
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY=True,
|
||||
CONVERSION_UPLOAD_ENABLED=False,
|
||||
CRISP_WEBSITE_ID="123",
|
||||
FRONTEND_CSS_URL="http://testcss/",
|
||||
FRONTEND_JS_URL="http://testjs/",
|
||||
@@ -56,6 +57,7 @@ def test_api_config(is_authenticated):
|
||||
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY": True,
|
||||
"CONVERSION_FILE_EXTENSIONS_ALLOWED": [".docx", ".md"],
|
||||
"CONVERSION_FILE_MAX_SIZE": 20971520,
|
||||
"CONVERSION_UPLOAD_ENABLED": False,
|
||||
"CRISP_WEBSITE_ID": "123",
|
||||
"ENVIRONMENT": "test",
|
||||
"FRONTEND_CSS_URL": "http://testcss/",
|
||||
|
||||
@@ -5,8 +5,6 @@ Unit tests for the Document model
|
||||
|
||||
import random
|
||||
import smtplib
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timezone as base_timezone
|
||||
from logging import Logger
|
||||
from unittest import mock
|
||||
|
||||
@@ -21,7 +19,6 @@ from django.utils import timezone
|
||||
import pytest
|
||||
|
||||
from core import factories, models
|
||||
from core.factories import DocumentFactory
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -90,8 +87,7 @@ def test_models_documents_tree_alphabet():
|
||||
|
||||
@pytest.mark.parametrize("depth", range(5))
|
||||
def test_models_documents_soft_delete(depth):
|
||||
"""
|
||||
Trying to delete a document that is already deleted or is a descendant of
|
||||
"""Trying to delete a document that is already deleted or is a descendant of
|
||||
a deleted document should raise an error.
|
||||
"""
|
||||
documents = []
|
||||
@@ -103,8 +99,6 @@ def test_models_documents_soft_delete(depth):
|
||||
)
|
||||
assert models.Document.objects.count() == depth + 1
|
||||
|
||||
document_pk_to_updated_at = {d.pk: d.updated_at for d in documents}
|
||||
|
||||
# Delete any one of the documents...
|
||||
deleted_document = random.choice(documents)
|
||||
deleted_document.soft_delete()
|
||||
@@ -112,26 +106,19 @@ def test_models_documents_soft_delete(depth):
|
||||
with pytest.raises(RuntimeError):
|
||||
documents[-1].soft_delete()
|
||||
|
||||
deleted_document.refresh_from_db()
|
||||
assert deleted_document.deleted_at is not None
|
||||
assert deleted_document.ancestors_deleted_at == deleted_document.deleted_at
|
||||
# updated_at is updated on the deleted document
|
||||
assert deleted_document.updated_at > document_pk_to_updated_at[deleted_document.pk]
|
||||
|
||||
descendants = deleted_document.get_descendants()
|
||||
for child in descendants:
|
||||
assert child.deleted_at is None
|
||||
assert child.ancestors_deleted_at is not None
|
||||
assert child.ancestors_deleted_at == deleted_document.deleted_at
|
||||
# updated_at is updated on descendants
|
||||
assert child.updated_at > document_pk_to_updated_at[child.pk]
|
||||
|
||||
ancestors = deleted_document.get_ancestors()
|
||||
for parent in ancestors:
|
||||
assert parent.deleted_at is None
|
||||
assert parent.ancestors_deleted_at is None
|
||||
# updated_at is not affected on parents
|
||||
assert parent.updated_at == document_pk_to_updated_at[parent.pk]
|
||||
|
||||
assert len(ancestors) + len(descendants) == depth
|
||||
|
||||
@@ -1432,20 +1419,16 @@ def test_models_documents_restore_tempering_with_instance():
|
||||
def test_models_documents_restore(django_assert_num_queries):
|
||||
"""The restore method should restore a soft-deleted document."""
|
||||
document = factories.DocumentFactory()
|
||||
|
||||
document.soft_delete()
|
||||
document.refresh_from_db()
|
||||
assert document.deleted_at is not None
|
||||
assert document.ancestors_deleted_at == document.deleted_at
|
||||
original_updated_after_delete = document.updated_at
|
||||
|
||||
with django_assert_num_queries(10):
|
||||
document.restore()
|
||||
document.refresh_from_db()
|
||||
assert document.deleted_at is None
|
||||
assert document.ancestors_deleted_at is None
|
||||
# updated_at is updated by restore
|
||||
assert original_updated_after_delete < document.updated_at
|
||||
assert document.ancestors_deleted_at == document.deleted_at
|
||||
|
||||
|
||||
def test_models_documents_restore_complex(django_assert_num_queries):
|
||||
@@ -1462,7 +1445,6 @@ def test_models_documents_restore_complex(django_assert_num_queries):
|
||||
document.refresh_from_db()
|
||||
child1.refresh_from_db()
|
||||
child2.refresh_from_db()
|
||||
|
||||
assert document.deleted_at is not None
|
||||
assert document.ancestors_deleted_at == document.deleted_at
|
||||
assert child1.ancestors_deleted_at == document.deleted_at
|
||||
@@ -1472,18 +1454,13 @@ def test_models_documents_restore_complex(django_assert_num_queries):
|
||||
grand_parent.soft_delete()
|
||||
grand_parent.refresh_from_db()
|
||||
parent.refresh_from_db()
|
||||
document.refresh_from_db()
|
||||
child1.refresh_from_db()
|
||||
child2.refresh_from_db()
|
||||
grand_parent_updated_at = grand_parent.updated_at
|
||||
document_updated_at = document.updated_at
|
||||
child1_updated_at = child2.updated_at
|
||||
child2_updated_at = child2.updated_at
|
||||
|
||||
assert grand_parent.deleted_at is not None
|
||||
assert grand_parent.ancestors_deleted_at == grand_parent.deleted_at
|
||||
assert parent.ancestors_deleted_at == grand_parent.deleted_at
|
||||
# item, child1 and child2 should not be affected
|
||||
document.refresh_from_db()
|
||||
child1.refresh_from_db()
|
||||
child2.refresh_from_db()
|
||||
assert document.deleted_at is not None
|
||||
assert document.ancestors_deleted_at == document.deleted_at
|
||||
assert child1.ancestors_deleted_at == document.deleted_at
|
||||
@@ -1492,23 +1469,15 @@ def test_models_documents_restore_complex(django_assert_num_queries):
|
||||
# Restore the item
|
||||
with django_assert_num_queries(14):
|
||||
document.restore()
|
||||
|
||||
document.refresh_from_db()
|
||||
child1.refresh_from_db()
|
||||
child2.refresh_from_db()
|
||||
grand_parent.refresh_from_db()
|
||||
|
||||
assert document.deleted_at is None
|
||||
assert document.ancestors_deleted_at == grand_parent.deleted_at
|
||||
# child 1 and child 2 should now have the same ancestors_deleted_at as the grand parent
|
||||
assert child1.ancestors_deleted_at == grand_parent.deleted_at
|
||||
assert child2.ancestors_deleted_at == grand_parent.deleted_at
|
||||
# updated_at is updated for document and children after restore
|
||||
assert document.updated_at > document_updated_at
|
||||
assert child1.updated_at > child1_updated_at
|
||||
assert child2.updated_at > child2_updated_at
|
||||
# grand_parent updated_at is not affected
|
||||
assert grand_parent.updated_at == grand_parent_updated_at
|
||||
|
||||
|
||||
def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
@@ -1516,37 +1485,31 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
grand_parent = factories.DocumentFactory()
|
||||
parent = factories.DocumentFactory(parent=grand_parent)
|
||||
document = factories.DocumentFactory(parent=parent)
|
||||
|
||||
child1 = factories.DocumentFactory(parent=document)
|
||||
child2 = factories.DocumentFactory(parent=document)
|
||||
|
||||
# Soft delete first the document
|
||||
document.soft_delete()
|
||||
|
||||
document.refresh_from_db()
|
||||
child1.refresh_from_db()
|
||||
child2.refresh_from_db()
|
||||
|
||||
assert document.deleted_at is not None
|
||||
assert document.ancestors_deleted_at == document.deleted_at
|
||||
assert child1.ancestors_deleted_at == document.deleted_at
|
||||
assert child2.ancestors_deleted_at == document.deleted_at
|
||||
|
||||
# Soft delete the grand_parent
|
||||
# Soft delete the grand parent
|
||||
grand_parent.soft_delete()
|
||||
|
||||
grand_parent.refresh_from_db()
|
||||
parent.refresh_from_db()
|
||||
document.refresh_from_db()
|
||||
child1.refresh_from_db()
|
||||
child2.refresh_from_db()
|
||||
original_parent_updated_at = parent.updated_at
|
||||
original_child1_updated_at = child1.updated_at
|
||||
original_child2_updated_at = child2.updated_at
|
||||
|
||||
assert grand_parent.deleted_at is not None
|
||||
assert grand_parent.ancestors_deleted_at == grand_parent.deleted_at
|
||||
assert parent.ancestors_deleted_at == grand_parent.deleted_at
|
||||
# item, child1 and child2 should not be affected
|
||||
document.refresh_from_db()
|
||||
child1.refresh_from_db()
|
||||
child2.refresh_from_db()
|
||||
assert document.deleted_at is not None
|
||||
assert document.ancestors_deleted_at == document.deleted_at
|
||||
assert child1.ancestors_deleted_at == document.deleted_at
|
||||
@@ -1562,20 +1525,14 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
document.refresh_from_db()
|
||||
child1.refresh_from_db()
|
||||
child2.refresh_from_db()
|
||||
|
||||
assert grand_parent.deleted_at is None
|
||||
assert grand_parent.ancestors_deleted_at is None
|
||||
assert parent.deleted_at is None
|
||||
assert parent.ancestors_deleted_at is None
|
||||
# parent should have updated_at updated (descendant of restored grand_parent)
|
||||
assert parent.updated_at > original_parent_updated_at
|
||||
assert document.deleted_at is not None
|
||||
assert document.ancestors_deleted_at == document.deleted_at
|
||||
# children are not restored and then there updated_at should not be affected
|
||||
assert child1.ancestors_deleted_at == document.deleted_at
|
||||
assert child1.updated_at == original_child1_updated_at
|
||||
assert child2.ancestors_deleted_at == document.deleted_at
|
||||
assert child2.updated_at == original_child2_updated_at
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -1734,82 +1691,3 @@ def test_models_documents_compute_ancestors_links_paths_mapping_structure(
|
||||
{"link_reach": sibling.link_reach, "link_role": sibling.link_role},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_models_documents_manager_time_filter_no_filters():
|
||||
"""Test time_filter with no filters returns all documents."""
|
||||
factories.DocumentFactory.create_batch(3)
|
||||
|
||||
queryset = models.Document.objects.filter_updated_at()
|
||||
|
||||
assert queryset.count() == 3
|
||||
|
||||
|
||||
def test_models_documents_manager_time_filter_oldest_updated_at():
|
||||
"""
|
||||
Test filtering by oldest_updated_at includes documents updated after or at
|
||||
lower_time_bound.
|
||||
"""
|
||||
lower_time_bound = datetime(2024, 2, 1, tzinfo=base_timezone.utc)
|
||||
|
||||
DocumentFactory(updated_at=lower_time_bound - timedelta(days=30))
|
||||
document_at_boundary = DocumentFactory(updated_at=lower_time_bound)
|
||||
document_recent = DocumentFactory(updated_at=lower_time_bound + timedelta(days=15))
|
||||
|
||||
queryset = models.Document.objects.filter_updated_at(
|
||||
lower_time_bound=lower_time_bound
|
||||
)
|
||||
|
||||
assert queryset.count() == 2
|
||||
assert sorted(queryset.values_list("pk", flat=True)) == sorted(
|
||||
[document_at_boundary.pk, document_recent.pk]
|
||||
)
|
||||
|
||||
|
||||
def test_models_documents_manager_time_filter_newest_updated_at():
|
||||
"""Test filtering by newest_updated_at includes documents updated before timestamp."""
|
||||
upper_time_bound = datetime(2024, 2, 1, tzinfo=base_timezone.utc)
|
||||
|
||||
document_old = DocumentFactory(updated_at=upper_time_bound - timedelta(days=30))
|
||||
document_at_boundary = DocumentFactory(updated_at=upper_time_bound)
|
||||
document_too_recent = DocumentFactory(
|
||||
updated_at=upper_time_bound + timedelta(days=15)
|
||||
)
|
||||
|
||||
queryset = models.Document.objects.filter_updated_at(
|
||||
upper_time_bound=upper_time_bound
|
||||
)
|
||||
|
||||
assert queryset.count() == 2
|
||||
assert document_old in queryset
|
||||
assert document_at_boundary in queryset
|
||||
assert document_too_recent not in queryset
|
||||
|
||||
|
||||
def test_models_documents_manager_time_filter_both_bounds():
|
||||
"""Test filtering with both oldest and newest bounds."""
|
||||
lower_time_bound = datetime(2024, 2, 1, tzinfo=base_timezone.utc)
|
||||
upper_time_bound = lower_time_bound + timedelta(days=30)
|
||||
|
||||
document_too_early = DocumentFactory(
|
||||
updated_at=lower_time_bound - timedelta(days=10)
|
||||
)
|
||||
document_in_window = DocumentFactory(
|
||||
updated_at=lower_time_bound + timedelta(days=5)
|
||||
)
|
||||
other_document_in_window = DocumentFactory(
|
||||
updated_at=lower_time_bound + timedelta(days=15)
|
||||
)
|
||||
document_too_late = DocumentFactory(
|
||||
updated_at=upper_time_bound + timedelta(days=10)
|
||||
)
|
||||
|
||||
queryset = models.Document.objects.filter_updated_at(
|
||||
lower_time_bound=lower_time_bound, upper_time_bound=upper_time_bound
|
||||
)
|
||||
|
||||
assert queryset.count() == 2
|
||||
assert document_too_early not in queryset
|
||||
assert document_in_window in queryset
|
||||
assert other_document_in_window in queryset
|
||||
assert document_too_late not in queryset
|
||||
|
||||
@@ -252,7 +252,7 @@ def test_services_search_indexers_index_errors(indexer_settings):
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPError):
|
||||
FindDocumentIndexer().index(models.Document.objects.all())
|
||||
FindDocumentIndexer().index()
|
||||
|
||||
|
||||
@patch.object(FindDocumentIndexer, "push")
|
||||
@@ -272,7 +272,7 @@ def test_services_search_indexers_batches_pass_only_batch_accesses(
|
||||
access = factories.UserDocumentAccessFactory(document=document)
|
||||
expected_user_subs[str(document.id)] = str(access.user.sub)
|
||||
|
||||
assert FindDocumentIndexer().index(models.Document.objects.all()) == 5
|
||||
assert FindDocumentIndexer().index() == 5
|
||||
|
||||
# Should be 3 batches: 2 + 2 + 1
|
||||
assert mock_push.call_count == 3
|
||||
@@ -310,7 +310,7 @@ def test_services_search_indexers_batch_size_argument(mock_push):
|
||||
access = factories.UserDocumentAccessFactory(document=document)
|
||||
expected_user_subs[str(document.id)] = str(access.user.sub)
|
||||
|
||||
assert FindDocumentIndexer().index(models.Document.objects.all(), batch_size=2) == 5
|
||||
assert FindDocumentIndexer().index(batch_size=2) == 5
|
||||
|
||||
# Should be 3 batches: 2 + 2 + 1
|
||||
assert mock_push.call_count == 3
|
||||
@@ -345,7 +345,7 @@ def test_services_search_indexers_ignore_empty_documents(mock_push):
|
||||
empty_title = factories.DocumentFactory(title="")
|
||||
empty_content = factories.DocumentFactory(content="")
|
||||
|
||||
assert FindDocumentIndexer().index(models.Document.objects.all()) == 3
|
||||
assert FindDocumentIndexer().index() == 3
|
||||
|
||||
assert mock_push.call_count == 1
|
||||
|
||||
@@ -373,7 +373,7 @@ def test_services_search_indexers_skip_empty_batches(mock_push, indexer_settings
|
||||
# Only empty docs
|
||||
factories.DocumentFactory.create_batch(5, content="", title="")
|
||||
|
||||
assert FindDocumentIndexer().index(models.Document.objects.all()) == 1
|
||||
assert FindDocumentIndexer().index() == 1
|
||||
assert mock_push.call_count == 1
|
||||
|
||||
results = [doc["id"] for doc in mock_push.call_args[0][0]]
|
||||
@@ -391,7 +391,7 @@ def test_services_search_indexers_ancestors_link_reach(mock_push):
|
||||
parent = factories.DocumentFactory(parent=grand_parent, link_reach="public")
|
||||
document = factories.DocumentFactory(parent=parent, link_reach="restricted")
|
||||
|
||||
assert FindDocumentIndexer().index(models.Document.objects.all()) == 4
|
||||
assert FindDocumentIndexer().index() == 4
|
||||
|
||||
results = {doc["id"]: doc for doc in mock_push.call_args[0][0]}
|
||||
assert len(results) == 4
|
||||
@@ -411,7 +411,7 @@ def test_services_search_indexers_ancestors_users(mock_push):
|
||||
parent = factories.DocumentFactory(parent=grand_parent, users=[user_p])
|
||||
document = factories.DocumentFactory(parent=parent, users=[user_d])
|
||||
|
||||
assert FindDocumentIndexer().index(models.Document.objects.all()) == 3
|
||||
assert FindDocumentIndexer().index() == 3
|
||||
|
||||
results = {doc["id"]: doc for doc in mock_push.call_args[0][0]}
|
||||
assert len(results) == 3
|
||||
@@ -432,7 +432,7 @@ def test_services_search_indexers_ancestors_teams(mock_push):
|
||||
parent = factories.DocumentFactory(parent=grand_parent, teams=["team_p"])
|
||||
document = factories.DocumentFactory(parent=parent, teams=["team_d"])
|
||||
|
||||
assert FindDocumentIndexer().index(models.Document.objects.all()) == 3
|
||||
assert FindDocumentIndexer().index() == 3
|
||||
|
||||
results = {doc["id"]: doc for doc in mock_push.call_args[0][0]}
|
||||
assert len(results) == 3
|
||||
|
||||
@@ -30,6 +30,20 @@ def test_invalid_settings_oidc_email_configuration():
|
||||
)
|
||||
|
||||
|
||||
def test_settings_conn_max_age_default():
|
||||
"""
|
||||
Test that DB_CONN_MAX_AGE defaults to 0 (close connections at the end of each request).
|
||||
|
||||
CONN_MAX_AGE is defined in the DATABASES class body and resolved by django-configurations
|
||||
at class setup time, so no post_setup() call is required here.
|
||||
"""
|
||||
|
||||
class TestSettings(Base):
|
||||
"""Fake test settings."""
|
||||
|
||||
assert TestSettings.DATABASES["default"]["CONN_MAX_AGE"] == 0
|
||||
|
||||
|
||||
def test_settings_psycopg_pool_not_enabled():
|
||||
"""
|
||||
Test that not changing DB_PSYCOPG_POOL_ENABLED should not configure psycopg in the DATABASES
|
||||
|
||||
@@ -99,6 +99,15 @@ class Base(Configuration):
|
||||
"localhost", environ_name="DB_HOST", environ_prefix=None
|
||||
),
|
||||
"PORT": values.Value(5432, environ_name="DB_PORT", environ_prefix=None),
|
||||
# Maximum lifetime of a database connection in seconds.
|
||||
# Use 0 to close connections at the end of each request.
|
||||
# Use None for unlimited persistent connections.
|
||||
# When using the psycopg pool (DB_PSYCOPG_POOL_ENABLED), set this to 0
|
||||
# so that connections are returned to the pool after each request.
|
||||
"CONN_MAX_AGE": values.IntegerValue(
|
||||
0, environ_name="DB_CONN_MAX_AGE", environ_prefix=None
|
||||
),
|
||||
"OPTIONS": {},
|
||||
# Psycopg pool can be configured in the post_setup method
|
||||
}
|
||||
}
|
||||
@@ -866,6 +875,9 @@ class Base(Configuration):
|
||||
DOCSPEC_API_URL = values.Value(environ_name="DOCSPEC_API_URL", environ_prefix=None)
|
||||
|
||||
# Imported file settings
|
||||
CONVERSION_UPLOAD_ENABLED = values.BooleanValue(
|
||||
False, environ_name="CONVERSION_UPLOAD_ENABLED", environ_prefix=None
|
||||
)
|
||||
CONVERSION_FILE_MAX_SIZE = values.IntegerValue(
|
||||
20 * MB,
|
||||
environ_name="CONVERSION_FILE_MAX_SIZE",
|
||||
@@ -1110,30 +1122,24 @@ class Base(Configuration):
|
||||
)
|
||||
|
||||
if psycopg_pool_enabled:
|
||||
cls.DATABASES["default"].update(
|
||||
{
|
||||
"OPTIONS": {
|
||||
# https://www.psycopg.org/psycopg3/docs/api/pool.html#psycopg_pool.ConnectionPool
|
||||
"pool": {
|
||||
"min_size": values.IntegerValue(
|
||||
4,
|
||||
environ_name="DB_PSYCOPG_POOL_MIN_SIZE",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"max_size": values.IntegerValue(
|
||||
None,
|
||||
environ_name="DB_PSYCOPG_POOL_MAX_SIZE",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"timeout": values.IntegerValue(
|
||||
3,
|
||||
environ_name="DB_PSYCOPG_POOL_TIMEOUT",
|
||||
environ_prefix=None,
|
||||
),
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
# https://www.psycopg.org/psycopg3/docs/api/pool.html#psycopg_pool.ConnectionPool
|
||||
cls.DATABASES["default"].setdefault("OPTIONS", {})["pool"] = {
|
||||
"min_size": values.IntegerValue(
|
||||
4,
|
||||
environ_name="DB_PSYCOPG_POOL_MIN_SIZE",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"max_size": values.IntegerValue(
|
||||
None,
|
||||
environ_name="DB_PSYCOPG_POOL_MAX_SIZE",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"timeout": values.IntegerValue(
|
||||
3,
|
||||
environ_name="DB_PSYCOPG_POOL_TIMEOUT",
|
||||
environ_prefix=None,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class Build(Base):
|
||||
|
||||
@@ -12,10 +12,7 @@ from drf_spectacular.views import (
|
||||
SpectacularSwaggerView,
|
||||
)
|
||||
|
||||
from core.admin import run_indexing_view
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/run-indexing/", run_indexing_view, name="run_indexing"),
|
||||
path("admin/", admin.site.urls),
|
||||
path("", include("core.urls")),
|
||||
]
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
|
||||
"PO-Revision-Date: 2026-03-13 16:53\n"
|
||||
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 16:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Breton\n"
|
||||
"Language: br_FR\n"
|
||||
@@ -46,36 +46,40 @@ msgstr "Gwezennadur"
|
||||
msgid "Title"
|
||||
msgstr "Titl"
|
||||
|
||||
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
|
||||
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Creator is me"
|
||||
msgstr "Me eo an aozer"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
msgid "Masked"
|
||||
msgstr "Kuzhet"
|
||||
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
|
||||
msgid "Favorite"
|
||||
msgstr "Sinedoù"
|
||||
|
||||
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
|
||||
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Ur restr nevez a zo bet krouet ganeoc'h!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
|
||||
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "C'hwi zo bet disklaeriet perc'henn ur restr nevez:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
|
||||
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
|
||||
msgid "This field is required."
|
||||
msgstr "Ar vaezienn-mañ a zo rekis."
|
||||
|
||||
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
|
||||
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "eilenn {title}"
|
||||
@@ -247,98 +251,98 @@ msgstr "implijer"
|
||||
msgid "users"
|
||||
msgstr "implijerien"
|
||||
|
||||
#: build/lib/core/models.py:378 core/models.py:378
|
||||
#: build/lib/core/models.py:376 core/models.py:376
|
||||
msgid "Active email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:379 core/models.py:379
|
||||
#: build/lib/core/models.py:377 core/models.py:377
|
||||
msgid "Email address to deactivate"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:406 core/models.py:406
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "Unique ID in the source file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
|
||||
#: core/models.py:708
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:413 core/models.py:413
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
|
||||
#: core/models.py:712
|
||||
msgid "Done"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
|
||||
#: core/models.py:713
|
||||
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
|
||||
#: core/models.py:711
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:423 core/models.py:423
|
||||
#: build/lib/core/models.py:421 core/models.py:421
|
||||
msgid "user reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 core/models.py:424
|
||||
#: build/lib/core/models.py:422 core/models.py:422
|
||||
msgid "user reconciliations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:662 core/models.py:662
|
||||
#: build/lib/core/models.py:660 core/models.py:660
|
||||
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
|
||||
" To confirm that you are the one who initiated the request\n"
|
||||
" and that this email belongs to you:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:668 core/models.py:668
|
||||
#: build/lib/core/models.py:666 core/models.py:666
|
||||
msgid "Confirm by clicking the link to start the reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
|
||||
#: core/models.py:779
|
||||
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
|
||||
#: core/models.py:777
|
||||
msgid "Click here"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:674 core/models.py:674
|
||||
#: build/lib/core/models.py:672 core/models.py:672
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:685 core/models.py:685
|
||||
#: build/lib/core/models.py:683 core/models.py:683
|
||||
msgid "Your reconciliation request has been processed.\n"
|
||||
" New documents are likely associated with your account:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:690 core/models.py:690
|
||||
#: build/lib/core/models.py:688 core/models.py:688
|
||||
msgid "Your accounts have been merged"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:695 core/models.py:695
|
||||
#: build/lib/core/models.py:693 core/models.py:693
|
||||
msgid "Click here to see"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:696 core/models.py:696
|
||||
#: build/lib/core/models.py:694 core/models.py:694
|
||||
msgid "See my documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:706 core/models.py:706
|
||||
#: build/lib/core/models.py:704 core/models.py:704
|
||||
msgid "CSV file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:711 core/models.py:711
|
||||
#: build/lib/core/models.py:709 core/models.py:709
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:721 core/models.py:721
|
||||
#: build/lib/core/models.py:719 core/models.py:719
|
||||
msgid "user reconciliation CSV import"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:722 core/models.py:722
|
||||
#: build/lib/core/models.py:720 core/models.py:720
|
||||
msgid "user reconciliation CSV imports"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:766 core/models.py:766
|
||||
#: build/lib/core/models.py:764 core/models.py:764
|
||||
#, python-brace-format
|
||||
msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" Reconciliation failed for the following email addresses:\n"
|
||||
@@ -347,175 +351,175 @@ msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" You can submit another request with the valid email addresses."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:774 core/models.py:774
|
||||
#: build/lib/core/models.py:772 core/models.py:772
|
||||
msgid "Reconciliation of your Docs accounts not completed"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:780 core/models.py:780
|
||||
#: build/lib/core/models.py:778 core/models.py:778
|
||||
msgid "Make a new request"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:879 core/models.py:879
|
||||
#: build/lib/core/models.py:877 core/models.py:877
|
||||
msgid "title"
|
||||
msgstr "titl"
|
||||
|
||||
#: build/lib/core/models.py:880 core/models.py:880
|
||||
#: build/lib/core/models.py:878 core/models.py:878
|
||||
msgid "excerpt"
|
||||
msgstr "bomm"
|
||||
|
||||
#: build/lib/core/models.py:929 core/models.py:929
|
||||
#: build/lib/core/models.py:927 core/models.py:927
|
||||
msgid "Document"
|
||||
msgstr "Restr"
|
||||
|
||||
#: build/lib/core/models.py:930 core/models.py:930
|
||||
#: build/lib/core/models.py:928 core/models.py:928
|
||||
msgid "Documents"
|
||||
msgstr "Restroù"
|
||||
|
||||
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
|
||||
#: core/models.py:942 core/models.py:1346
|
||||
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
|
||||
#: core/models.py:940 core/models.py:1345
|
||||
msgid "Untitled Document"
|
||||
msgstr "Restr hep titl"
|
||||
|
||||
#: build/lib/core/models.py:1347 core/models.py:1347
|
||||
#: build/lib/core/models.py:1346 core/models.py:1346
|
||||
msgid "Open"
|
||||
msgstr "Digeriñ"
|
||||
|
||||
#: build/lib/core/models.py:1382 core/models.py:1382
|
||||
#: build/lib/core/models.py:1381 core/models.py:1381
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} en deus rannet ur restr ganeoc'h!"
|
||||
|
||||
#: build/lib/core/models.py:1386 core/models.py:1386
|
||||
#: build/lib/core/models.py:1385 core/models.py:1385
|
||||
#, 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:1392 core/models.py:1392
|
||||
#: build/lib/core/models.py:1391 core/models.py:1391
|
||||
#, 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:1493 core/models.py:1493
|
||||
#: build/lib/core/models.py:1492 core/models.py:1492
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Roud liamm ar restr/an implijer"
|
||||
|
||||
#: build/lib/core/models.py:1494 core/models.py:1494
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Roudoù liamm ar restr/an implijer"
|
||||
|
||||
#: build/lib/core/models.py:1500 core/models.py:1500
|
||||
#: build/lib/core/models.py:1499 core/models.py:1499
|
||||
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:1523 core/models.py:1523
|
||||
#: build/lib/core/models.py:1522 core/models.py:1522
|
||||
msgid "Document favorite"
|
||||
msgstr "Restr muiañ-karet"
|
||||
|
||||
#: build/lib/core/models.py:1524 core/models.py:1524
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
msgid "Document favorites"
|
||||
msgstr "Restroù muiañ-karet"
|
||||
|
||||
#: build/lib/core/models.py:1530 core/models.py:1530
|
||||
#: build/lib/core/models.py:1529 core/models.py:1529
|
||||
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:1552 core/models.py:1552
|
||||
#: build/lib/core/models.py:1551 core/models.py:1551
|
||||
msgid "Document/user relation"
|
||||
msgstr "Liamm restr/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1553 core/models.py:1553
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
msgid "Document/user relations"
|
||||
msgstr "Liammoù restr/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1559 core/models.py:1559
|
||||
#: build/lib/core/models.py:1558 core/models.py:1558
|
||||
msgid "This user is already in this document."
|
||||
msgstr "An implijer-mañ a zo dija er restr-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1565 core/models.py:1565
|
||||
#: build/lib/core/models.py:1564 core/models.py:1564
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1571 core/models.py:1571
|
||||
#: build/lib/core/models.py:1570 core/models.py:1570
|
||||
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:1722 core/models.py:1722
|
||||
#: build/lib/core/models.py:1721 core/models.py:1721
|
||||
msgid "Document ask for access"
|
||||
msgstr "Goulenn tizhout ar restr"
|
||||
|
||||
#: build/lib/core/models.py:1723 core/models.py:1723
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Goulennoù tizhout ar restr"
|
||||
|
||||
#: build/lib/core/models.py:1729 core/models.py:1729
|
||||
#: build/lib/core/models.py:1728 core/models.py:1728
|
||||
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:1786 core/models.py:1786
|
||||
#: build/lib/core/models.py:1785 core/models.py:1785
|
||||
#, 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:1790 core/models.py:1790
|
||||
#: build/lib/core/models.py:1789 core/models.py:1789
|
||||
#, 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:1796 core/models.py:1796
|
||||
#: build/lib/core/models.py:1795 core/models.py:1795
|
||||
#, 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:1838 core/models.py:1838
|
||||
#: build/lib/core/models.py:1837 core/models.py:1837
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1839 core/models.py:1839
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
|
||||
#: core/models.py:1842 core/models.py:1894
|
||||
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
|
||||
#: core/models.py:1841 core/models.py:1893
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
#: build/lib/core/models.py:1888 core/models.py:1888
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1890 core/models.py:1890
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1939 core/models.py:1939
|
||||
#: build/lib/core/models.py:1938 core/models.py:1938
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
#: build/lib/core/models.py:1942 core/models.py:1942
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1944 core/models.py:1944
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1954 core/models.py:1954
|
||||
#: build/lib/core/models.py:1953 core/models.py:1953
|
||||
msgid "email address"
|
||||
msgstr "postel"
|
||||
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
#: build/lib/core/models.py:1972 core/models.py:1972
|
||||
msgid "Document invitation"
|
||||
msgstr "Pedadenn d'ur restr"
|
||||
|
||||
#: build/lib/core/models.py:1974 core/models.py:1974
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
msgid "Document invitations"
|
||||
msgstr "Pedadennoù d'ur restr"
|
||||
|
||||
#: build/lib/core/models.py:1994 core/models.py:1994
|
||||
#: build/lib/core/models.py:1993 core/models.py:1993
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
|
||||
|
||||
#: build/lib/impress/settings.py:702 impress/settings.py:702
|
||||
#: build/lib/impress/settings.py:808 impress/settings.py:808
|
||||
msgid "Docs AI"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
|
||||
"PO-Revision-Date: 2026-03-13 16:53\n"
|
||||
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 16:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -28,11 +28,11 @@ msgstr "Berechtigungen"
|
||||
|
||||
#: build/lib/core/admin.py:55 core/admin.py:55
|
||||
msgid "Important dates"
|
||||
msgstr "Wichtige Daten"
|
||||
msgstr "Wichtige Termine"
|
||||
|
||||
#: build/lib/core/admin.py:112 core/admin.py:112
|
||||
msgid "Import job created and queued."
|
||||
msgstr ""
|
||||
msgstr "Import-Job erstellt und in der Warteschlange."
|
||||
|
||||
#: build/lib/core/admin.py:116 core/admin.py:116
|
||||
msgid "Process selected user reconciliations"
|
||||
@@ -46,36 +46,40 @@ msgstr "Baumstruktur"
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
|
||||
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
|
||||
msgid "Search"
|
||||
msgstr "Suchen"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Creator is me"
|
||||
msgstr "Ersteller bin ich"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
msgid "Masked"
|
||||
msgstr "Maskiert"
|
||||
|
||||
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
|
||||
msgid "Favorite"
|
||||
msgstr "Favorit"
|
||||
|
||||
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
|
||||
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
|
||||
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Sie sind Besitzer eines neuen Dokuments:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
|
||||
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
msgstr "Dies ist ein Pflichtfeld."
|
||||
|
||||
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
msgstr "Der Zugriff auf den Link '%(link_reach)s' ist aufgrund der Konfiguration übergeordneter Dokumente nicht erlaubt."
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
|
||||
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "Kopie von {title}"
|
||||
@@ -92,7 +96,7 @@ msgstr "Lesen"
|
||||
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
|
||||
#: core/choices.py:44
|
||||
msgid "Commenter"
|
||||
msgstr ""
|
||||
msgstr "Kommentieren"
|
||||
|
||||
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
|
||||
#: core/choices.py:45
|
||||
@@ -173,11 +177,11 @@ msgstr "Wir konnten keinen Benutzer mit diesem Abo finden, aber die E-Mail-Adres
|
||||
|
||||
#: build/lib/core/models.py:141 core/models.py:141
|
||||
msgid "sub"
|
||||
msgstr "unter"
|
||||
msgstr "sub"
|
||||
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Required. 255 characters or fewer. ASCII characters only."
|
||||
msgstr ""
|
||||
msgstr "Pflichtfeld. 255 Zeichen oder weniger. Buchstaben (nur ASCII), Ziffern und die Zeichen @/-/_/."
|
||||
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "full name"
|
||||
@@ -233,11 +237,11 @@ msgstr "Ob dieser Benutzer als aktiviert behandelt werden soll. Deaktivieren Sie
|
||||
|
||||
#: build/lib/core/models.py:197 core/models.py:197
|
||||
msgid "first connection status"
|
||||
msgstr ""
|
||||
msgstr "Status der ersten Verbindung"
|
||||
|
||||
#: build/lib/core/models.py:199 core/models.py:199
|
||||
msgid "Whether the user has completed the first connection process."
|
||||
msgstr ""
|
||||
msgstr "Gibt an, ob der Benutzer die Prozedur der ersten Verbindung abgeschlossen hat."
|
||||
|
||||
#: build/lib/core/models.py:209 core/models.py:209
|
||||
msgid "user"
|
||||
@@ -247,98 +251,98 @@ msgstr "Benutzer"
|
||||
msgid "users"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: build/lib/core/models.py:378 core/models.py:378
|
||||
#: build/lib/core/models.py:376 core/models.py:376
|
||||
msgid "Active email address"
|
||||
msgstr ""
|
||||
msgstr "Aktive E-Mail-Adresse"
|
||||
|
||||
#: build/lib/core/models.py:379 core/models.py:379
|
||||
#: build/lib/core/models.py:377 core/models.py:377
|
||||
msgid "Email address to deactivate"
|
||||
msgstr ""
|
||||
msgstr "Zu deaktivierende E-Mail-Adresse"
|
||||
|
||||
#: build/lib/core/models.py:406 core/models.py:406
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "Unique ID in the source file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
|
||||
#: core/models.py:708
|
||||
msgid "Pending"
|
||||
msgstr "Ausstehend"
|
||||
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Ready"
|
||||
msgstr "Bereit"
|
||||
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:413 core/models.py:413
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
|
||||
#: core/models.py:712
|
||||
msgid "Done"
|
||||
msgstr ""
|
||||
msgstr "Fertig"
|
||||
|
||||
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
|
||||
#: core/models.py:713
|
||||
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
|
||||
#: core/models.py:711
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
msgstr "Fehler"
|
||||
|
||||
#: build/lib/core/models.py:423 core/models.py:423
|
||||
#: build/lib/core/models.py:421 core/models.py:421
|
||||
msgid "user reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 core/models.py:424
|
||||
#: build/lib/core/models.py:422 core/models.py:422
|
||||
msgid "user reconciliations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:662 core/models.py:662
|
||||
#: build/lib/core/models.py:660 core/models.py:660
|
||||
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
|
||||
" To confirm that you are the one who initiated the request\n"
|
||||
" and that this email belongs to you:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:668 core/models.py:668
|
||||
#: build/lib/core/models.py:666 core/models.py:666
|
||||
msgid "Confirm by clicking the link to start the reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
|
||||
#: core/models.py:779
|
||||
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
|
||||
#: core/models.py:777
|
||||
msgid "Click here"
|
||||
msgstr ""
|
||||
msgstr "Klicken Sie hier"
|
||||
|
||||
#: build/lib/core/models.py:674 core/models.py:674
|
||||
#: build/lib/core/models.py:672 core/models.py:672
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
msgstr "Bestätigen Sie"
|
||||
|
||||
#: build/lib/core/models.py:685 core/models.py:685
|
||||
#: build/lib/core/models.py:683 core/models.py:683
|
||||
msgid "Your reconciliation request has been processed.\n"
|
||||
" New documents are likely associated with your account:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:690 core/models.py:690
|
||||
#: build/lib/core/models.py:688 core/models.py:688
|
||||
msgid "Your accounts have been merged"
|
||||
msgstr ""
|
||||
msgstr "Ihre Konten wurden zusammengelegt"
|
||||
|
||||
#: build/lib/core/models.py:695 core/models.py:695
|
||||
#: build/lib/core/models.py:693 core/models.py:693
|
||||
msgid "Click here to see"
|
||||
msgstr ""
|
||||
msgstr "Klicken Sie hier um zu sehen"
|
||||
|
||||
#: build/lib/core/models.py:696 core/models.py:696
|
||||
#: build/lib/core/models.py:694 core/models.py:694
|
||||
msgid "See my documents"
|
||||
msgstr ""
|
||||
msgstr "Meine Dokumente einsehen"
|
||||
|
||||
#: build/lib/core/models.py:706 core/models.py:706
|
||||
#: build/lib/core/models.py:704 core/models.py:704
|
||||
msgid "CSV file"
|
||||
msgstr ""
|
||||
msgstr "CSV-Datei"
|
||||
|
||||
#: build/lib/core/models.py:711 core/models.py:711
|
||||
#: build/lib/core/models.py:709 core/models.py:709
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
msgstr "Wird ausgeführt"
|
||||
|
||||
#: build/lib/core/models.py:721 core/models.py:721
|
||||
#: build/lib/core/models.py:719 core/models.py:719
|
||||
msgid "user reconciliation CSV import"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:722 core/models.py:722
|
||||
#: build/lib/core/models.py:720 core/models.py:720
|
||||
msgid "user reconciliation CSV imports"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:766 core/models.py:766
|
||||
#: build/lib/core/models.py:764 core/models.py:764
|
||||
#, python-brace-format
|
||||
msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" Reconciliation failed for the following email addresses:\n"
|
||||
@@ -347,177 +351,177 @@ msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" You can submit another request with the valid email addresses."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:774 core/models.py:774
|
||||
#: build/lib/core/models.py:772 core/models.py:772
|
||||
msgid "Reconciliation of your Docs accounts not completed"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:780 core/models.py:780
|
||||
#: build/lib/core/models.py:778 core/models.py:778
|
||||
msgid "Make a new request"
|
||||
msgstr ""
|
||||
msgstr "Neue Anfrage erstellen"
|
||||
|
||||
#: build/lib/core/models.py:879 core/models.py:879
|
||||
#: build/lib/core/models.py:877 core/models.py:877
|
||||
msgid "title"
|
||||
msgstr "Titel"
|
||||
|
||||
#: build/lib/core/models.py:880 core/models.py:880
|
||||
#: build/lib/core/models.py:878 core/models.py:878
|
||||
msgid "excerpt"
|
||||
msgstr "Auszug"
|
||||
|
||||
#: build/lib/core/models.py:929 core/models.py:929
|
||||
#: build/lib/core/models.py:927 core/models.py:927
|
||||
msgid "Document"
|
||||
msgstr "Dokument"
|
||||
|
||||
#: build/lib/core/models.py:930 core/models.py:930
|
||||
#: build/lib/core/models.py:928 core/models.py:928
|
||||
msgid "Documents"
|
||||
msgstr "Dokumente"
|
||||
|
||||
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
|
||||
#: core/models.py:942 core/models.py:1346
|
||||
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
|
||||
#: core/models.py:940 core/models.py:1345
|
||||
msgid "Untitled Document"
|
||||
msgstr "Unbenanntes Dokument"
|
||||
|
||||
#: build/lib/core/models.py:1347 core/models.py:1347
|
||||
#: build/lib/core/models.py:1346 core/models.py:1346
|
||||
msgid "Open"
|
||||
msgstr "Öffnen"
|
||||
|
||||
#: build/lib/core/models.py:1382 core/models.py:1382
|
||||
#: build/lib/core/models.py:1381 core/models.py:1381
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
|
||||
|
||||
#: build/lib/core/models.py:1386 core/models.py:1386
|
||||
#: build/lib/core/models.py:1385 core/models.py:1385
|
||||
#, 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:1392 core/models.py:1392
|
||||
#: build/lib/core/models.py:1391 core/models.py:1391
|
||||
#, 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:1493 core/models.py:1493
|
||||
#: build/lib/core/models.py:1492 core/models.py:1492
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: build/lib/core/models.py:1494 core/models.py:1494
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: build/lib/core/models.py:1500 core/models.py:1500
|
||||
#: build/lib/core/models.py:1499 core/models.py:1499
|
||||
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:1523 core/models.py:1523
|
||||
#: build/lib/core/models.py:1522 core/models.py:1522
|
||||
msgid "Document favorite"
|
||||
msgstr "Dokumentenfavorit"
|
||||
|
||||
#: build/lib/core/models.py:1524 core/models.py:1524
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
msgid "Document favorites"
|
||||
msgstr "Dokumentfavoriten"
|
||||
|
||||
#: build/lib/core/models.py:1530 core/models.py:1530
|
||||
#: build/lib/core/models.py:1529 core/models.py:1529
|
||||
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:1552 core/models.py:1552
|
||||
#: build/lib/core/models.py:1551 core/models.py:1551
|
||||
msgid "Document/user relation"
|
||||
msgstr "Dokument/Benutzerbeziehung"
|
||||
|
||||
#: build/lib/core/models.py:1553 core/models.py:1553
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
msgid "Document/user relations"
|
||||
msgstr "Dokument/Benutzerbeziehungen"
|
||||
|
||||
#: build/lib/core/models.py:1559 core/models.py:1559
|
||||
#: build/lib/core/models.py:1558 core/models.py:1558
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
|
||||
|
||||
#: build/lib/core/models.py:1565 core/models.py:1565
|
||||
#: build/lib/core/models.py:1564 core/models.py:1564
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
|
||||
|
||||
#: build/lib/core/models.py:1571 core/models.py:1571
|
||||
#: build/lib/core/models.py:1570 core/models.py:1570
|
||||
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:1722 core/models.py:1722
|
||||
#: build/lib/core/models.py:1721 core/models.py:1721
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
msgstr "Dokument um Zugriff bitten"
|
||||
|
||||
#: build/lib/core/models.py:1723 core/models.py:1723
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
msgstr "Dokumentenabfragen"
|
||||
|
||||
#: build/lib/core/models.py:1729 core/models.py:1729
|
||||
#: build/lib/core/models.py:1728 core/models.py:1728
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
msgstr "Dieser Benutzer hat bereits um Zugang zu diesem Dokument gebeten."
|
||||
|
||||
#: build/lib/core/models.py:1786 core/models.py:1786
|
||||
#: build/lib/core/models.py:1785 core/models.py:1785
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
msgstr "{name} möchte Zugriff auf ein Dokument erhalten!"
|
||||
|
||||
#: build/lib/core/models.py:1790 core/models.py:1790
|
||||
#: build/lib/core/models.py:1789 core/models.py:1789
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
msgstr "{name} möchte auf das folgende Dokument zugreifen:"
|
||||
|
||||
#: build/lib/core/models.py:1796 core/models.py:1796
|
||||
#: build/lib/core/models.py:1795 core/models.py:1795
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
msgstr "{name} bittet um Zugang zum Dokument: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1837 core/models.py:1837
|
||||
msgid "Thread"
|
||||
msgstr "Thread"
|
||||
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1839 core/models.py:1839
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
msgstr "Threads"
|
||||
|
||||
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
|
||||
#: core/models.py:1842 core/models.py:1894
|
||||
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
|
||||
#: core/models.py:1841 core/models.py:1893
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
msgstr "Gast"
|
||||
|
||||
#: build/lib/core/models.py:1888 core/models.py:1888
|
||||
msgid "Comment"
|
||||
msgstr "Kommentar"
|
||||
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1890 core/models.py:1890
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
msgstr "Kommentare"
|
||||
|
||||
#: build/lib/core/models.py:1939 core/models.py:1939
|
||||
#: build/lib/core/models.py:1938 core/models.py:1938
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
#: build/lib/core/models.py:1942 core/models.py:1942
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
msgstr "Reaktion"
|
||||
|
||||
#: build/lib/core/models.py:1944 core/models.py:1944
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
msgstr "Reaktionen"
|
||||
|
||||
#: build/lib/core/models.py:1954 core/models.py:1954
|
||||
#: build/lib/core/models.py:1953 core/models.py:1953
|
||||
msgid "email address"
|
||||
msgstr "E-Mail-Adresse"
|
||||
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
#: build/lib/core/models.py:1972 core/models.py:1972
|
||||
msgid "Document invitation"
|
||||
msgstr "Einladung zum Dokument"
|
||||
|
||||
#: build/lib/core/models.py:1974 core/models.py:1974
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
msgid "Document invitations"
|
||||
msgstr "Dokumenteinladungen"
|
||||
|
||||
#: build/lib/core/models.py:1994 core/models.py:1994
|
||||
#: build/lib/core/models.py:1993 core/models.py:1993
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
|
||||
|
||||
#: build/lib/impress/settings.py:702 impress/settings.py:702
|
||||
#: build/lib/impress/settings.py:808 impress/settings.py:808
|
||||
msgid "Docs AI"
|
||||
msgstr ""
|
||||
msgstr "Docs AI"
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
|
||||
548
src/backend/locale/el_GR/LC_MESSAGES/django.po
Normal file
548
src/backend/locale/el_GR/LC_MESSAGES/django.po
Normal file
@@ -0,0 +1,548 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 16:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Greek\n"
|
||||
"Language: el_GR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: lasuite-docs\n"
|
||||
"X-Crowdin-Project-ID: 754523\n"
|
||||
"X-Crowdin-Language: el\n"
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 18\n"
|
||||
|
||||
#: build/lib/core/admin.py:30 core/admin.py:30
|
||||
msgid "Personal info"
|
||||
msgstr "Προσωπικές πληροφορίες"
|
||||
|
||||
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
|
||||
#: core/admin.py:161
|
||||
msgid "Permissions"
|
||||
msgstr "Δικαιώματα"
|
||||
|
||||
#: build/lib/core/admin.py:55 core/admin.py:55
|
||||
msgid "Important dates"
|
||||
msgstr "Σημαντικές ημερομηνίες"
|
||||
|
||||
#: build/lib/core/admin.py:112 core/admin.py:112
|
||||
msgid "Import job created and queued."
|
||||
msgstr "Η εργασία εισαγωγής δημιουργήθηκε και μπήκε στην ουρά."
|
||||
|
||||
#: build/lib/core/admin.py:116 core/admin.py:116
|
||||
msgid "Process selected user reconciliations"
|
||||
msgstr "Επεξεργασία επιλεγμένων συμφωνιών χρηστών"
|
||||
|
||||
#: build/lib/core/admin.py:171 core/admin.py:171
|
||||
msgid "Tree structure"
|
||||
msgstr "Δομή δέντρου"
|
||||
|
||||
#: build/lib/core/api/filters.py:48 core/api/filters.py:48
|
||||
msgid "Title"
|
||||
msgstr "Τίτλος"
|
||||
|
||||
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
|
||||
msgid "Search"
|
||||
msgstr "Αναζήτηση"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Creator is me"
|
||||
msgstr "Δημιουργός είμαι εγώ"
|
||||
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
msgid "Masked"
|
||||
msgstr "Με κάλυψη"
|
||||
|
||||
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
|
||||
msgid "Favorite"
|
||||
msgstr "Αγαπημένο"
|
||||
|
||||
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Ένα νέο έγγραφο δημιουργήθηκε εκ μέρους σας!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Σας παραχωρήθηκε η ιδιοκτησία ενός νέου εγγράφου:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
|
||||
msgid "This field is required."
|
||||
msgstr "Αυτό το πεδίο είναι υποχρεωτικό."
|
||||
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr "Η εμβέλεια συνδέσμου '%(link_reach)s' δεν επιτρέπεται βάσει της διαμόρφωσης του γονικού εγγράφου."
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "αντίγραφο του {title}"
|
||||
|
||||
#: build/lib/core/apps.py:12 core/apps.py:12
|
||||
msgid "Impress core application"
|
||||
msgstr "Κεντρική εφαρμογή Impress"
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
|
||||
#: core/choices.py:43
|
||||
msgid "Reader"
|
||||
msgstr "Αναγνώστης"
|
||||
|
||||
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
|
||||
#: core/choices.py:44
|
||||
msgid "Commenter"
|
||||
msgstr "Σχολιαστής"
|
||||
|
||||
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
|
||||
#: core/choices.py:45
|
||||
msgid "Editor"
|
||||
msgstr "Συντάκτης"
|
||||
|
||||
#: build/lib/core/choices.py:46 core/choices.py:46
|
||||
msgid "Administrator"
|
||||
msgstr "Διαχειριστής"
|
||||
|
||||
#: build/lib/core/choices.py:47 core/choices.py:47
|
||||
msgid "Owner"
|
||||
msgstr "Ιδιοκτήτης"
|
||||
|
||||
#: build/lib/core/choices.py:58 core/choices.py:58
|
||||
msgid "Restricted"
|
||||
msgstr "Περιορισμένο"
|
||||
|
||||
#: build/lib/core/choices.py:62 core/choices.py:62
|
||||
msgid "Authenticated"
|
||||
msgstr "Πιστοποιημένο"
|
||||
|
||||
#: build/lib/core/choices.py:64 core/choices.py:64
|
||||
msgid "Public"
|
||||
msgstr "Δημόσιο"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr "Πρώτο θυγατρικό"
|
||||
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
msgid "Last child"
|
||||
msgstr "Τελευταίο θυγατρικό"
|
||||
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
msgid "First sibling"
|
||||
msgstr "Πρώτο αδελφό"
|
||||
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
msgid "Last sibling"
|
||||
msgstr "Τελευταίο αδελφό"
|
||||
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
msgid "Left"
|
||||
msgstr "Αριστερά"
|
||||
|
||||
#: build/lib/core/enums.py:41 core/enums.py:41
|
||||
msgid "Right"
|
||||
msgstr "Δεξιά"
|
||||
|
||||
#: build/lib/core/models.py:80 core/models.py:80
|
||||
msgid "id"
|
||||
msgstr "αναγνωριστικό"
|
||||
|
||||
#: build/lib/core/models.py:81 core/models.py:81
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "πρωτεύον κλειδί για την εγγραφή ως UUID"
|
||||
|
||||
#: build/lib/core/models.py:87 core/models.py:87
|
||||
msgid "created on"
|
||||
msgstr "δημιουργήθηκε στις"
|
||||
|
||||
#: build/lib/core/models.py:88 core/models.py:88
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "ημερομηνία και ώρα δημιουργίας μιας εγγραφής"
|
||||
|
||||
#: build/lib/core/models.py:93 core/models.py:93
|
||||
msgid "updated on"
|
||||
msgstr "ενημερώθηκε στις"
|
||||
|
||||
#: build/lib/core/models.py:94 core/models.py:94
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "ημερομηνία και ώρα τελευταίας ενημέρωσης μιας εγγραφής"
|
||||
|
||||
#: 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 "Δεν μπορέσαμε να βρούμε χρήστη με αυτό το sub, αλλά το email σχετίζεται ήδη με έναν εγγεγραμμένο χρήστη."
|
||||
|
||||
#: build/lib/core/models.py:141 core/models.py:141
|
||||
msgid "sub"
|
||||
msgstr "sub (αναγνωριστικό υποκειμένου)"
|
||||
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Required. 255 characters or fewer. ASCII characters only."
|
||||
msgstr "Υποχρεωτικό. 255 χαρακτήρες ή λιγότεροι. Μόνο χαρακτήρες ASCII."
|
||||
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "full name"
|
||||
msgstr "πλήρες όνομα"
|
||||
|
||||
#: build/lib/core/models.py:152 core/models.py:152
|
||||
msgid "short name"
|
||||
msgstr "σύντομο όνομα"
|
||||
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr "διεύθυνση email ταυτότητας"
|
||||
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr "διεύθυνση email διαχειριστή"
|
||||
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "language"
|
||||
msgstr "γλώσσα"
|
||||
|
||||
#: 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:176 core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Η ζώνη ώρας στην οποία ο χρήστης θέλει να βλέπει την ώρα."
|
||||
|
||||
#: build/lib/core/models.py:179 core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "συσκευή"
|
||||
|
||||
#: 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:184 core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "κατάσταση προσωπικού"
|
||||
|
||||
#: 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:189 core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "ενεργός"
|
||||
|
||||
#: 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:197 core/models.py:197
|
||||
msgid "first connection status"
|
||||
msgstr "πρώτη κατάσταση σύνδεσης"
|
||||
|
||||
#: build/lib/core/models.py:199 core/models.py:199
|
||||
msgid "Whether the user has completed the first connection process."
|
||||
msgstr "Εάν ο χρήστης έχει ολοκληρώσει τη διαδικασία της πρώτης σύνδεσης."
|
||||
|
||||
#: build/lib/core/models.py:209 core/models.py:209
|
||||
msgid "user"
|
||||
msgstr "χρήστης"
|
||||
|
||||
#: build/lib/core/models.py:210 core/models.py:210
|
||||
msgid "users"
|
||||
msgstr "χρήστες"
|
||||
|
||||
#: build/lib/core/models.py:376 core/models.py:376
|
||||
msgid "Active email address"
|
||||
msgstr "Ενεργή διεύθυνση email"
|
||||
|
||||
#: build/lib/core/models.py:377 core/models.py:377
|
||||
msgid "Email address to deactivate"
|
||||
msgstr "Διεύθυνση email για απενεργοποίηση"
|
||||
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "Unique ID in the source file"
|
||||
msgstr "Μοναδικό αναγνωριστικό στο πηγαίο αρχείο"
|
||||
|
||||
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
|
||||
#: core/models.py:708
|
||||
msgid "Pending"
|
||||
msgstr "Σε εκκρεμότητα"
|
||||
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Ready"
|
||||
msgstr "Έτοιμο"
|
||||
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
msgid "Done"
|
||||
msgstr "Ολοκληρώθηκε"
|
||||
|
||||
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
|
||||
#: core/models.py:711
|
||||
msgid "Error"
|
||||
msgstr "Σφάλμα"
|
||||
|
||||
#: build/lib/core/models.py:421 core/models.py:421
|
||||
msgid "user reconciliation"
|
||||
msgstr "συμφωνία χρήστη"
|
||||
|
||||
#: build/lib/core/models.py:422 core/models.py:422
|
||||
msgid "user reconciliations"
|
||||
msgstr "συμφωνία χρηστών"
|
||||
|
||||
#: build/lib/core/models.py:660 core/models.py:660
|
||||
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
|
||||
" To confirm that you are the one who initiated the request\n"
|
||||
" and that this email belongs to you:"
|
||||
msgstr "Έχετε ζητήσει έναν συνδυασμό των λογαριασμών χρήστη σας στα Έγγραφα.\n"
|
||||
" Για να επιβεβαιώσετε ότι είστε εκείνος που ξεκίνησε το αίτημα\n"
|
||||
" και ότι αυτό το email ανήκει σε σας:"
|
||||
|
||||
#: build/lib/core/models.py:666 core/models.py:666
|
||||
msgid "Confirm by clicking the link to start the reconciliation"
|
||||
msgstr "Επιβεβαιώστε κάνοντας κλικ στο σύνδεσμο για να ξεκινήσει η συμφωνία"
|
||||
|
||||
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
|
||||
#: core/models.py:777
|
||||
msgid "Click here"
|
||||
msgstr "Κάντε κλικ εδώ"
|
||||
|
||||
#: build/lib/core/models.py:672 core/models.py:672
|
||||
msgid "Confirm"
|
||||
msgstr "Επιβεβαίωση"
|
||||
|
||||
#: build/lib/core/models.py:683 core/models.py:683
|
||||
msgid "Your reconciliation request has been processed.\n"
|
||||
" New documents are likely associated with your account:"
|
||||
msgstr "Το αίτημά σας για συμφωνία έχει επεξεργαστεί.\n"
|
||||
" Νέα έγγραφα είναι πιθανό να σχετίζονται με τον λογαριασμό σας:"
|
||||
|
||||
#: build/lib/core/models.py:688 core/models.py:688
|
||||
msgid "Your accounts have been merged"
|
||||
msgstr "Οι λογαριασμοί σας έχουν συγχωνευθεί"
|
||||
|
||||
#: build/lib/core/models.py:693 core/models.py:693
|
||||
msgid "Click here to see"
|
||||
msgstr "Κάντε κλικ εδώ για να δείτε"
|
||||
|
||||
#: build/lib/core/models.py:694 core/models.py:694
|
||||
msgid "See my documents"
|
||||
msgstr "Δείτε τα έγγραφά μου"
|
||||
|
||||
#: build/lib/core/models.py:704 core/models.py:704
|
||||
msgid "CSV file"
|
||||
msgstr "Αρχείο CSV"
|
||||
|
||||
#: build/lib/core/models.py:709 core/models.py:709
|
||||
msgid "Running"
|
||||
msgstr "Εκτελείται"
|
||||
|
||||
#: build/lib/core/models.py:719 core/models.py:719
|
||||
msgid "user reconciliation CSV import"
|
||||
msgstr "εισαγωγή CSV συμφωνίας χρηστών"
|
||||
|
||||
#: build/lib/core/models.py:720 core/models.py:720
|
||||
msgid "user reconciliation CSV imports"
|
||||
msgstr "εισαγωγές CSV συμφωνίας χρηστών"
|
||||
|
||||
#: build/lib/core/models.py:764 core/models.py:764
|
||||
#, python-brace-format
|
||||
msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" Reconciliation failed for the following email addresses:\n"
|
||||
" {recipient_email}, {other_email}.\n"
|
||||
" Please check for typos.\n"
|
||||
" You can submit another request with the valid email addresses."
|
||||
msgstr "Το αίτημά σας για επαλήθευση δεν ολοκληρώθηκε με επιτυχία.\n"
|
||||
" Η επαλήθευση απέτυχε για τις ακόλουθες διευθύνσεις email:\n"
|
||||
" {recipient_email}, {other_email}.\n"
|
||||
" Παρακαλούμε ελέγξτε αν υπάρχουν τυπογραφικά λάθη.\n"
|
||||
" Μπορείτε να υποβάλετε ένα νέο αίτημα με τις σωστές διευθύνσεις email."
|
||||
|
||||
#: build/lib/core/models.py:772 core/models.py:772
|
||||
msgid "Reconciliation of your Docs accounts not completed"
|
||||
msgstr "Η συμφωνία των λογαριασμών σας Docs δεν ολοκληρώθηκε"
|
||||
|
||||
#: build/lib/core/models.py:778 core/models.py:778
|
||||
msgid "Make a new request"
|
||||
msgstr "Κάντε ένα νέο αίτημα"
|
||||
|
||||
#: build/lib/core/models.py:877 core/models.py:877
|
||||
msgid "title"
|
||||
msgstr "τίτλος"
|
||||
|
||||
#: build/lib/core/models.py:878 core/models.py:878
|
||||
msgid "excerpt"
|
||||
msgstr "απόσπασμα"
|
||||
|
||||
#: build/lib/core/models.py:927 core/models.py:927
|
||||
msgid "Document"
|
||||
msgstr "Έγγραφο"
|
||||
|
||||
#: build/lib/core/models.py:928 core/models.py:928
|
||||
msgid "Documents"
|
||||
msgstr "Έγγραφα"
|
||||
|
||||
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
|
||||
#: core/models.py:940 core/models.py:1345
|
||||
msgid "Untitled Document"
|
||||
msgstr "Έγγραφο χωρίς τίτλο"
|
||||
|
||||
#: build/lib/core/models.py:1346 core/models.py:1346
|
||||
msgid "Open"
|
||||
msgstr "Άνοιγμα"
|
||||
|
||||
#: build/lib/core/models.py:1381 core/models.py:1381
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "Ο/Η {name} μοιράστηκε ένα έγγραφο μαζί σας!"
|
||||
|
||||
#: build/lib/core/models.py:1385 core/models.py:1385
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "Ο/Η {name} σας προσκάλεσε με τον ρόλο \"{role}\" στο ακόλουθο έγγραφο:"
|
||||
|
||||
#: build/lib/core/models.py:1391 core/models.py:1391
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "Ο/Η {name} μοιράστηκε ένα έγγραφο μαζί σας: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1492 core/models.py:1492
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Ίχνος συνδέσμου εγγράφου/χρήστη"
|
||||
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Ίχνη συνδέσμου εγγράφου/χρήστη"
|
||||
|
||||
#: build/lib/core/models.py:1499 core/models.py:1499
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Ένα ίχνος συνδέσμου υπάρχει ήδη για αυτό το έγγραφο/χρήστη."
|
||||
|
||||
#: build/lib/core/models.py:1522 core/models.py:1522
|
||||
msgid "Document favorite"
|
||||
msgstr "Αγαπημένο έγγραφο"
|
||||
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
msgid "Document favorites"
|
||||
msgstr "Αγαπημένα έγγραφα"
|
||||
|
||||
#: build/lib/core/models.py:1529 core/models.py:1529
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Αυτό το έγγραφο στοχεύεται ήδη από μια σχέση αγαπημένου για τον ίδιο χρήστη."
|
||||
|
||||
#: build/lib/core/models.py:1551 core/models.py:1551
|
||||
msgid "Document/user relation"
|
||||
msgstr "Σχέση εγγράφου/χρήστη"
|
||||
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
msgid "Document/user relations"
|
||||
msgstr "Σχέσεις εγγράφου/χρήστη"
|
||||
|
||||
#: build/lib/core/models.py:1558 core/models.py:1558
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Αυτός ο χρήστης συμμετέχει ήδη σε αυτό το έγγραφο."
|
||||
|
||||
#: build/lib/core/models.py:1564 core/models.py:1564
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Αυτή η ομάδα συμμετέχει ήδη σε αυτό το έγγραφο."
|
||||
|
||||
#: build/lib/core/models.py:1570 core/models.py:1570
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Πρέπει να οριστεί είτε χρήστης είτε ομάδα, όχι και τα δύο."
|
||||
|
||||
#: build/lib/core/models.py:1721 core/models.py:1721
|
||||
msgid "Document ask for access"
|
||||
msgstr "Αίτημα πρόσβασης σε έγγραφο"
|
||||
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Αιτήματα πρόσβασης σε έγγραφα"
|
||||
|
||||
#: build/lib/core/models.py:1728 core/models.py:1728
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "Αυτός ο χρήστης έχει ήδη ζητήσει πρόσβαση σε αυτό το έγγραφο."
|
||||
|
||||
#: build/lib/core/models.py:1785 core/models.py:1785
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "Ο/Η {name} θα ήθελε πρόσβαση σε ένα έγγραφο!"
|
||||
|
||||
#: build/lib/core/models.py:1789 core/models.py:1789
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "Ο/Η {name} θα ήθελε πρόσβαση στο ακόλουθο έγγραφο:"
|
||||
|
||||
#: build/lib/core/models.py:1795 core/models.py:1795
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "Ο/Η {name} ζητά πρόσβαση στο έγγραφο: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1837 core/models.py:1837
|
||||
msgid "Thread"
|
||||
msgstr "Νήμα"
|
||||
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
msgid "Threads"
|
||||
msgstr "Νήματα"
|
||||
|
||||
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
|
||||
#: core/models.py:1841 core/models.py:1893
|
||||
msgid "Anonymous"
|
||||
msgstr "Ανώνυμος"
|
||||
|
||||
#: build/lib/core/models.py:1888 core/models.py:1888
|
||||
msgid "Comment"
|
||||
msgstr "Σχόλιο"
|
||||
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
msgid "Comments"
|
||||
msgstr "Σχόλια"
|
||||
|
||||
#: build/lib/core/models.py:1938 core/models.py:1938
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr "Αυτό το emoji έχει χρησιμοποιηθεί ήδη ως αντίδραση σε αυτό το σχόλιο."
|
||||
|
||||
#: build/lib/core/models.py:1942 core/models.py:1942
|
||||
msgid "Reaction"
|
||||
msgstr "Αντίδραση"
|
||||
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
msgid "Reactions"
|
||||
msgstr "Αντιδράσεις"
|
||||
|
||||
#: build/lib/core/models.py:1953 core/models.py:1953
|
||||
msgid "email address"
|
||||
msgstr "διεύθυνση email"
|
||||
|
||||
#: build/lib/core/models.py:1972 core/models.py:1972
|
||||
msgid "Document invitation"
|
||||
msgstr "Πρόσκληση σε έγγραφο"
|
||||
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
msgid "Document invitations"
|
||||
msgstr "Προσκλήσεις εγγράφου"
|
||||
|
||||
#: build/lib/core/models.py:1993 core/models.py:1993
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Αυτό το email σχετίζεται ήδη με έναν εγγεγραμμένο χρήστη."
|
||||
|
||||
#: build/lib/impress/settings.py:808 impress/settings.py:808
|
||||
msgid "Docs AI"
|
||||
msgstr "Τεχνητή Νοημοσύνη (AI) Docs"
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Λογότυπο email"
|
||||
|
||||
#: core/templates/mail/html/template.html:219
|
||||
#: core/templates/mail/text/template.txt:14
|
||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||
msgstr " Docs, το νέο απαραίτητο εργαλείο σας για την οργάνωση, τον διαμοιρασμό και τη συνεργασία στα έγγραφά σας ως ομάδα. "
|
||||
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr " Σας προσφέρεται από την %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
|
||||
"PO-Revision-Date: 2026-03-13 16:53\n"
|
||||
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 16:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_US\n"
|
||||
@@ -46,36 +46,40 @@ msgstr ""
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
|
||||
msgid "Creator is me"
|
||||
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Masked"
|
||||
msgid "Creator is me"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
|
||||
msgid "Favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
|
||||
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
|
||||
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
|
||||
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
|
||||
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
@@ -247,98 +251,98 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:378 core/models.py:378
|
||||
#: build/lib/core/models.py:376 core/models.py:376
|
||||
msgid "Active email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:379 core/models.py:379
|
||||
#: build/lib/core/models.py:377 core/models.py:377
|
||||
msgid "Email address to deactivate"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:406 core/models.py:406
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "Unique ID in the source file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
|
||||
#: core/models.py:708
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:413 core/models.py:413
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
|
||||
#: core/models.py:712
|
||||
msgid "Done"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
|
||||
#: core/models.py:713
|
||||
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
|
||||
#: core/models.py:711
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:423 core/models.py:423
|
||||
#: build/lib/core/models.py:421 core/models.py:421
|
||||
msgid "user reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 core/models.py:424
|
||||
#: build/lib/core/models.py:422 core/models.py:422
|
||||
msgid "user reconciliations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:662 core/models.py:662
|
||||
#: build/lib/core/models.py:660 core/models.py:660
|
||||
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
|
||||
" To confirm that you are the one who initiated the request\n"
|
||||
" and that this email belongs to you:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:668 core/models.py:668
|
||||
#: build/lib/core/models.py:666 core/models.py:666
|
||||
msgid "Confirm by clicking the link to start the reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
|
||||
#: core/models.py:779
|
||||
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
|
||||
#: core/models.py:777
|
||||
msgid "Click here"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:674 core/models.py:674
|
||||
#: build/lib/core/models.py:672 core/models.py:672
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:685 core/models.py:685
|
||||
#: build/lib/core/models.py:683 core/models.py:683
|
||||
msgid "Your reconciliation request has been processed.\n"
|
||||
" New documents are likely associated with your account:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:690 core/models.py:690
|
||||
#: build/lib/core/models.py:688 core/models.py:688
|
||||
msgid "Your accounts have been merged"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:695 core/models.py:695
|
||||
#: build/lib/core/models.py:693 core/models.py:693
|
||||
msgid "Click here to see"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:696 core/models.py:696
|
||||
#: build/lib/core/models.py:694 core/models.py:694
|
||||
msgid "See my documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:706 core/models.py:706
|
||||
#: build/lib/core/models.py:704 core/models.py:704
|
||||
msgid "CSV file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:711 core/models.py:711
|
||||
#: build/lib/core/models.py:709 core/models.py:709
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:721 core/models.py:721
|
||||
#: build/lib/core/models.py:719 core/models.py:719
|
||||
msgid "user reconciliation CSV import"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:722 core/models.py:722
|
||||
#: build/lib/core/models.py:720 core/models.py:720
|
||||
msgid "user reconciliation CSV imports"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:766 core/models.py:766
|
||||
#: build/lib/core/models.py:764 core/models.py:764
|
||||
#, python-brace-format
|
||||
msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" Reconciliation failed for the following email addresses:\n"
|
||||
@@ -347,175 +351,175 @@ msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" You can submit another request with the valid email addresses."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:774 core/models.py:774
|
||||
#: build/lib/core/models.py:772 core/models.py:772
|
||||
msgid "Reconciliation of your Docs accounts not completed"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:780 core/models.py:780
|
||||
#: build/lib/core/models.py:778 core/models.py:778
|
||||
msgid "Make a new request"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:879 core/models.py:879
|
||||
#: build/lib/core/models.py:877 core/models.py:877
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:880 core/models.py:880
|
||||
#: build/lib/core/models.py:878 core/models.py:878
|
||||
msgid "excerpt"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:929 core/models.py:929
|
||||
#: build/lib/core/models.py:927 core/models.py:927
|
||||
msgid "Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:930 core/models.py:930
|
||||
#: build/lib/core/models.py:928 core/models.py:928
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
|
||||
#: core/models.py:942 core/models.py:1346
|
||||
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
|
||||
#: core/models.py:940 core/models.py:1345
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1347 core/models.py:1347
|
||||
#: build/lib/core/models.py:1346 core/models.py:1346
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1382 core/models.py:1382
|
||||
#: build/lib/core/models.py:1381 core/models.py:1381
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1386 core/models.py:1386
|
||||
#: build/lib/core/models.py:1385 core/models.py:1385
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1392 core/models.py:1392
|
||||
#: build/lib/core/models.py:1391 core/models.py:1391
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
#: build/lib/core/models.py:1492 core/models.py:1492
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1494 core/models.py:1494
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1500 core/models.py:1500
|
||||
#: build/lib/core/models.py:1499 core/models.py:1499
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
#: build/lib/core/models.py:1522 core/models.py:1522
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1524 core/models.py:1524
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1530 core/models.py:1530
|
||||
#: build/lib/core/models.py:1529 core/models.py:1529
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
#: build/lib/core/models.py:1551 core/models.py:1551
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1553 core/models.py:1553
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1559 core/models.py:1559
|
||||
#: build/lib/core/models.py:1558 core/models.py:1558
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1565 core/models.py:1565
|
||||
#: build/lib/core/models.py:1564 core/models.py:1564
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1571 core/models.py:1571
|
||||
#: build/lib/core/models.py:1570 core/models.py:1570
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
#: build/lib/core/models.py:1721 core/models.py:1721
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1723 core/models.py:1723
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1729 core/models.py:1729
|
||||
#: build/lib/core/models.py:1728 core/models.py:1728
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1786 core/models.py:1786
|
||||
#: build/lib/core/models.py:1785 core/models.py:1785
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1790 core/models.py:1790
|
||||
#: build/lib/core/models.py:1789 core/models.py:1789
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1796 core/models.py:1796
|
||||
#: build/lib/core/models.py:1795 core/models.py:1795
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
#: build/lib/core/models.py:1837 core/models.py:1837
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1839 core/models.py:1839
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
|
||||
#: core/models.py:1842 core/models.py:1894
|
||||
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
|
||||
#: core/models.py:1841 core/models.py:1893
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
#: build/lib/core/models.py:1888 core/models.py:1888
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1890 core/models.py:1890
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1939 core/models.py:1939
|
||||
#: build/lib/core/models.py:1938 core/models.py:1938
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
#: build/lib/core/models.py:1942 core/models.py:1942
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1944 core/models.py:1944
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1954 core/models.py:1954
|
||||
#: build/lib/core/models.py:1953 core/models.py:1953
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
#: build/lib/core/models.py:1972 core/models.py:1972
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1974 core/models.py:1974
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1994 core/models.py:1994
|
||||
#: build/lib/core/models.py:1993 core/models.py:1993
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/impress/settings.py:702 impress/settings.py:702
|
||||
#: build/lib/impress/settings.py:808 impress/settings.py:808
|
||||
msgid "Docs AI"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
|
||||
"PO-Revision-Date: 2026-03-13 16:53\n"
|
||||
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 16:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Language: es_ES\n"
|
||||
@@ -46,36 +46,40 @@ msgstr "Estructura en árbol"
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
|
||||
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
|
||||
msgid "Search"
|
||||
msgstr "Buscar"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Creator is me"
|
||||
msgstr "Yo soy el creador"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
msgid "Masked"
|
||||
msgstr "Enmascarado"
|
||||
|
||||
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
|
||||
msgid "Favorite"
|
||||
msgstr "Favorito"
|
||||
|
||||
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
|
||||
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "¡Un nuevo documento se ha creado por ti!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
|
||||
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Se le ha concedido la propiedad de un nuevo documento :"
|
||||
|
||||
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
|
||||
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
|
||||
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copia de {title}"
|
||||
@@ -247,98 +251,98 @@ msgstr "usuario"
|
||||
msgid "users"
|
||||
msgstr "usuarios"
|
||||
|
||||
#: build/lib/core/models.py:378 core/models.py:378
|
||||
#: build/lib/core/models.py:376 core/models.py:376
|
||||
msgid "Active email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:379 core/models.py:379
|
||||
#: build/lib/core/models.py:377 core/models.py:377
|
||||
msgid "Email address to deactivate"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:406 core/models.py:406
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "Unique ID in the source file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
|
||||
#: core/models.py:708
|
||||
msgid "Pending"
|
||||
msgstr "Pending"
|
||||
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Ready"
|
||||
msgstr "Listo"
|
||||
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:413 core/models.py:413
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
|
||||
#: core/models.py:712
|
||||
msgid "Done"
|
||||
msgstr ""
|
||||
msgstr "Terminado"
|
||||
|
||||
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
|
||||
#: core/models.py:713
|
||||
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
|
||||
#: core/models.py:711
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
msgstr "Error"
|
||||
|
||||
#: build/lib/core/models.py:423 core/models.py:423
|
||||
#: build/lib/core/models.py:421 core/models.py:421
|
||||
msgid "user reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 core/models.py:424
|
||||
#: build/lib/core/models.py:422 core/models.py:422
|
||||
msgid "user reconciliations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:662 core/models.py:662
|
||||
#: build/lib/core/models.py:660 core/models.py:660
|
||||
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
|
||||
" To confirm that you are the one who initiated the request\n"
|
||||
" and that this email belongs to you:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:668 core/models.py:668
|
||||
#: build/lib/core/models.py:666 core/models.py:666
|
||||
msgid "Confirm by clicking the link to start the reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
|
||||
#: core/models.py:779
|
||||
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
|
||||
#: core/models.py:777
|
||||
msgid "Click here"
|
||||
msgstr ""
|
||||
msgstr "Haga click aquí"
|
||||
|
||||
#: build/lib/core/models.py:674 core/models.py:674
|
||||
#: build/lib/core/models.py:672 core/models.py:672
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
msgstr "Confirmar"
|
||||
|
||||
#: build/lib/core/models.py:685 core/models.py:685
|
||||
#: build/lib/core/models.py:683 core/models.py:683
|
||||
msgid "Your reconciliation request has been processed.\n"
|
||||
" New documents are likely associated with your account:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:690 core/models.py:690
|
||||
#: build/lib/core/models.py:688 core/models.py:688
|
||||
msgid "Your accounts have been merged"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:695 core/models.py:695
|
||||
#: build/lib/core/models.py:693 core/models.py:693
|
||||
msgid "Click here to see"
|
||||
msgstr ""
|
||||
msgstr "Haz clic aquí para ver"
|
||||
|
||||
#: build/lib/core/models.py:696 core/models.py:696
|
||||
#: build/lib/core/models.py:694 core/models.py:694
|
||||
msgid "See my documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:706 core/models.py:706
|
||||
#: build/lib/core/models.py:704 core/models.py:704
|
||||
msgid "CSV file"
|
||||
msgstr ""
|
||||
msgstr "Archivo CSV"
|
||||
|
||||
#: build/lib/core/models.py:711 core/models.py:711
|
||||
#: build/lib/core/models.py:709 core/models.py:709
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
msgstr "En ejecución"
|
||||
|
||||
#: build/lib/core/models.py:721 core/models.py:721
|
||||
#: build/lib/core/models.py:719 core/models.py:719
|
||||
msgid "user reconciliation CSV import"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:722 core/models.py:722
|
||||
#: build/lib/core/models.py:720 core/models.py:720
|
||||
msgid "user reconciliation CSV imports"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:766 core/models.py:766
|
||||
#: build/lib/core/models.py:764 core/models.py:764
|
||||
#, python-brace-format
|
||||
msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" Reconciliation failed for the following email addresses:\n"
|
||||
@@ -347,177 +351,177 @@ msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" You can submit another request with the valid email addresses."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:774 core/models.py:774
|
||||
#: build/lib/core/models.py:772 core/models.py:772
|
||||
msgid "Reconciliation of your Docs accounts not completed"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:780 core/models.py:780
|
||||
#: build/lib/core/models.py:778 core/models.py:778
|
||||
msgid "Make a new request"
|
||||
msgstr ""
|
||||
msgstr "Hacer un nuevo pedido"
|
||||
|
||||
#: build/lib/core/models.py:879 core/models.py:879
|
||||
#: build/lib/core/models.py:877 core/models.py:877
|
||||
msgid "title"
|
||||
msgstr "título"
|
||||
|
||||
#: build/lib/core/models.py:880 core/models.py:880
|
||||
#: build/lib/core/models.py:878 core/models.py:878
|
||||
msgid "excerpt"
|
||||
msgstr "resumen"
|
||||
|
||||
#: build/lib/core/models.py:929 core/models.py:929
|
||||
#: build/lib/core/models.py:927 core/models.py:927
|
||||
msgid "Document"
|
||||
msgstr "Documento"
|
||||
|
||||
#: build/lib/core/models.py:930 core/models.py:930
|
||||
#: build/lib/core/models.py:928 core/models.py:928
|
||||
msgid "Documents"
|
||||
msgstr "Documentos"
|
||||
|
||||
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
|
||||
#: core/models.py:942 core/models.py:1346
|
||||
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
|
||||
#: core/models.py:940 core/models.py:1345
|
||||
msgid "Untitled Document"
|
||||
msgstr "Documento sin título"
|
||||
|
||||
#: build/lib/core/models.py:1347 core/models.py:1347
|
||||
#: build/lib/core/models.py:1346 core/models.py:1346
|
||||
msgid "Open"
|
||||
msgstr "Abrir"
|
||||
|
||||
#: build/lib/core/models.py:1382 core/models.py:1382
|
||||
#: build/lib/core/models.py:1381 core/models.py:1381
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "¡{name} ha compartido un documento contigo!"
|
||||
|
||||
#: build/lib/core/models.py:1386 core/models.py:1386
|
||||
#: build/lib/core/models.py:1385 core/models.py:1385
|
||||
#, 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:1392 core/models.py:1392
|
||||
#: build/lib/core/models.py:1391 core/models.py:1391
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} ha compartido un documento contigo: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
#: build/lib/core/models.py:1492 core/models.py:1492
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Traza del enlace de documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1494 core/models.py:1494
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Trazas del enlace de documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1500 core/models.py:1500
|
||||
#: build/lib/core/models.py:1499 core/models.py:1499
|
||||
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:1523 core/models.py:1523
|
||||
#: build/lib/core/models.py:1522 core/models.py:1522
|
||||
msgid "Document favorite"
|
||||
msgstr "Documento favorito"
|
||||
|
||||
#: build/lib/core/models.py:1524 core/models.py:1524
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
msgid "Document favorites"
|
||||
msgstr "Documentos favoritos"
|
||||
|
||||
#: build/lib/core/models.py:1530 core/models.py:1530
|
||||
#: build/lib/core/models.py:1529 core/models.py:1529
|
||||
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:1552 core/models.py:1552
|
||||
#: build/lib/core/models.py:1551 core/models.py:1551
|
||||
msgid "Document/user relation"
|
||||
msgstr "Relación documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1553 core/models.py:1553
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
msgid "Document/user relations"
|
||||
msgstr "Relaciones documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1559 core/models.py:1559
|
||||
#: build/lib/core/models.py:1558 core/models.py:1558
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Este usuario ya forma parte del documento."
|
||||
|
||||
#: build/lib/core/models.py:1565 core/models.py:1565
|
||||
#: build/lib/core/models.py:1564 core/models.py:1564
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Este equipo ya forma parte del documento."
|
||||
|
||||
#: build/lib/core/models.py:1571 core/models.py:1571
|
||||
#: build/lib/core/models.py:1570 core/models.py:1570
|
||||
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:1722 core/models.py:1722
|
||||
#: build/lib/core/models.py:1721 core/models.py:1721
|
||||
msgid "Document ask for access"
|
||||
msgstr "Solicitud de acceso"
|
||||
|
||||
#: build/lib/core/models.py:1723 core/models.py:1723
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Solicitud de accesos"
|
||||
|
||||
#: build/lib/core/models.py:1729 core/models.py:1729
|
||||
#: build/lib/core/models.py:1728 core/models.py:1728
|
||||
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:1786 core/models.py:1786
|
||||
#: build/lib/core/models.py:1785 core/models.py:1785
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "¡{name} desea acceder a un documento!"
|
||||
|
||||
#: build/lib/core/models.py:1790 core/models.py:1790
|
||||
#: build/lib/core/models.py:1789 core/models.py:1789
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} desea acceso al siguiente documento:"
|
||||
|
||||
#: build/lib/core/models.py:1796 core/models.py:1796
|
||||
#: build/lib/core/models.py:1795 core/models.py:1795
|
||||
#, 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:1838 core/models.py:1838
|
||||
#: build/lib/core/models.py:1837 core/models.py:1837
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
msgstr "Thread"
|
||||
|
||||
#: build/lib/core/models.py:1839 core/models.py:1839
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
msgstr "Threads"
|
||||
|
||||
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
|
||||
#: core/models.py:1842 core/models.py:1894
|
||||
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
|
||||
#: core/models.py:1841 core/models.py:1893
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
msgstr "Anónimo"
|
||||
|
||||
#: build/lib/core/models.py:1888 core/models.py:1888
|
||||
msgid "Comment"
|
||||
msgstr "Comentario"
|
||||
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1890 core/models.py:1890
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
msgstr "Comentarios"
|
||||
|
||||
#: build/lib/core/models.py:1939 core/models.py:1939
|
||||
#: build/lib/core/models.py:1938 core/models.py:1938
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
#: build/lib/core/models.py:1942 core/models.py:1942
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
msgstr "Reacción"
|
||||
|
||||
#: build/lib/core/models.py:1944 core/models.py:1944
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
msgstr "Reacciones"
|
||||
|
||||
#: build/lib/core/models.py:1954 core/models.py:1954
|
||||
#: build/lib/core/models.py:1953 core/models.py:1953
|
||||
msgid "email address"
|
||||
msgstr "dirección de correo electrónico"
|
||||
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
#: build/lib/core/models.py:1972 core/models.py:1972
|
||||
msgid "Document invitation"
|
||||
msgstr "Invitación al documento"
|
||||
|
||||
#: build/lib/core/models.py:1974 core/models.py:1974
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
msgid "Document invitations"
|
||||
msgstr "Invitaciones a documentos"
|
||||
|
||||
#: build/lib/core/models.py:1994 core/models.py:1994
|
||||
#: build/lib/core/models.py:1993 core/models.py:1993
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Este correo electrónico está asociado a un usuario registrado."
|
||||
|
||||
#: build/lib/impress/settings.py:702 impress/settings.py:702
|
||||
#: build/lib/impress/settings.py:808 impress/settings.py:808
|
||||
msgid "Docs AI"
|
||||
msgstr ""
|
||||
msgstr "Docs AI"
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
|
||||
"PO-Revision-Date: 2026-03-13 16:53\n"
|
||||
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 16:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr_FR\n"
|
||||
@@ -46,36 +46,40 @@ msgstr "Arborescence"
|
||||
msgid "Title"
|
||||
msgstr "Titre"
|
||||
|
||||
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
|
||||
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
|
||||
msgid "Search"
|
||||
msgstr "Recherche"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Creator is me"
|
||||
msgstr "Je suis l'auteur"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
msgid "Masked"
|
||||
msgstr "Masqué"
|
||||
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
|
||||
msgid "Favorite"
|
||||
msgstr "Favoris"
|
||||
|
||||
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
|
||||
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Un nouveau document a été créé pour vous !"
|
||||
|
||||
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
|
||||
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Vous avez été déclaré propriétaire d'un nouveau document :"
|
||||
|
||||
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
|
||||
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
|
||||
msgid "This field is required."
|
||||
msgstr "Ce champ est obligatoire."
|
||||
|
||||
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr "La portée du lien '%(link_reach)s' n'est pas autorisée en fonction de la configuration du document parent."
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
|
||||
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copie de {title}"
|
||||
@@ -247,46 +251,46 @@ msgstr "utilisateur"
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
#: build/lib/core/models.py:378 core/models.py:378
|
||||
#: build/lib/core/models.py:376 core/models.py:376
|
||||
msgid "Active email address"
|
||||
msgstr "Adresse email active"
|
||||
|
||||
#: build/lib/core/models.py:379 core/models.py:379
|
||||
#: build/lib/core/models.py:377 core/models.py:377
|
||||
msgid "Email address to deactivate"
|
||||
msgstr "Adresse email à désactiver"
|
||||
|
||||
#: build/lib/core/models.py:406 core/models.py:406
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "Unique ID in the source file"
|
||||
msgstr "Identifiant unique dans le fichier source"
|
||||
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
|
||||
#: core/models.py:708
|
||||
msgid "Pending"
|
||||
msgstr "En attente"
|
||||
|
||||
#: build/lib/core/models.py:413 core/models.py:413
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Ready"
|
||||
msgstr "Prêt"
|
||||
|
||||
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
|
||||
#: core/models.py:712
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
msgid "Done"
|
||||
msgstr "Terminé"
|
||||
|
||||
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
|
||||
#: core/models.py:713
|
||||
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
|
||||
#: core/models.py:711
|
||||
msgid "Error"
|
||||
msgstr "Erreur"
|
||||
|
||||
#: build/lib/core/models.py:423 core/models.py:423
|
||||
#: build/lib/core/models.py:421 core/models.py:421
|
||||
msgid "user reconciliation"
|
||||
msgstr "rapprochement de l'utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:424 core/models.py:424
|
||||
#: build/lib/core/models.py:422 core/models.py:422
|
||||
msgid "user reconciliations"
|
||||
msgstr "rapprochements de l'utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:662 core/models.py:662
|
||||
#: build/lib/core/models.py:660 core/models.py:660
|
||||
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
|
||||
" To confirm that you are the one who initiated the request\n"
|
||||
" and that this email belongs to you:"
|
||||
@@ -294,54 +298,54 @@ msgstr "Vous avez demandé un rapprochement de vos comptes utilisateur sur Docs.
|
||||
" Pour confirmer que vous êtes bien à l'origine de cette demande\n"
|
||||
" et que cet e-mail vous appartient :"
|
||||
|
||||
#: build/lib/core/models.py:668 core/models.py:668
|
||||
#: build/lib/core/models.py:666 core/models.py:666
|
||||
msgid "Confirm by clicking the link to start the reconciliation"
|
||||
msgstr "Confirmez en cliquant sur le lien pour commencer le rapprochement"
|
||||
|
||||
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
|
||||
#: core/models.py:779
|
||||
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
|
||||
#: core/models.py:777
|
||||
msgid "Click here"
|
||||
msgstr "Cliquez ici"
|
||||
|
||||
#: build/lib/core/models.py:674 core/models.py:674
|
||||
#: build/lib/core/models.py:672 core/models.py:672
|
||||
msgid "Confirm"
|
||||
msgstr "Confirmer"
|
||||
|
||||
#: build/lib/core/models.py:685 core/models.py:685
|
||||
#: build/lib/core/models.py:683 core/models.py:683
|
||||
msgid "Your reconciliation request has been processed.\n"
|
||||
" New documents are likely associated with your account:"
|
||||
msgstr "Votre demande de rapprochement a été traitée.\n"
|
||||
" De nouveaux documents sont probablement associés à votre compte :"
|
||||
|
||||
#: build/lib/core/models.py:690 core/models.py:690
|
||||
#: build/lib/core/models.py:688 core/models.py:688
|
||||
msgid "Your accounts have been merged"
|
||||
msgstr "Vos comptes ont été fusionnés"
|
||||
|
||||
#: build/lib/core/models.py:695 core/models.py:695
|
||||
#: build/lib/core/models.py:693 core/models.py:693
|
||||
msgid "Click here to see"
|
||||
msgstr "Cliquez ici pour voir"
|
||||
|
||||
#: build/lib/core/models.py:696 core/models.py:696
|
||||
#: build/lib/core/models.py:694 core/models.py:694
|
||||
msgid "See my documents"
|
||||
msgstr "Voir mes documents"
|
||||
|
||||
#: build/lib/core/models.py:706 core/models.py:706
|
||||
#: build/lib/core/models.py:704 core/models.py:704
|
||||
msgid "CSV file"
|
||||
msgstr "Fichier CSV"
|
||||
|
||||
#: build/lib/core/models.py:711 core/models.py:711
|
||||
#: build/lib/core/models.py:709 core/models.py:709
|
||||
msgid "Running"
|
||||
msgstr "En cours"
|
||||
|
||||
#: build/lib/core/models.py:721 core/models.py:721
|
||||
#: build/lib/core/models.py:719 core/models.py:719
|
||||
msgid "user reconciliation CSV import"
|
||||
msgstr "importation CSV de rapprochement utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:722 core/models.py:722
|
||||
#: build/lib/core/models.py:720 core/models.py:720
|
||||
msgid "user reconciliation CSV imports"
|
||||
msgstr "importations CSV de rapprochement utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:766 core/models.py:766
|
||||
#: build/lib/core/models.py:764 core/models.py:764
|
||||
#, python-brace-format
|
||||
msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" Reconciliation failed for the following email addresses:\n"
|
||||
@@ -354,175 +358,175 @@ msgstr "Votre demande de rapprochement n'a pas abouti.\n"
|
||||
" Veuillez vérifier qu'il n'y a pas de fautes de frappe.\n"
|
||||
" Vous pouvez envoyer une nouvelle demande avec des adresses e-mail valides."
|
||||
|
||||
#: build/lib/core/models.py:774 core/models.py:774
|
||||
#: build/lib/core/models.py:772 core/models.py:772
|
||||
msgid "Reconciliation of your Docs accounts not completed"
|
||||
msgstr "Le rapprochement de vos comptes Docs n'est pas terminé"
|
||||
|
||||
#: build/lib/core/models.py:780 core/models.py:780
|
||||
#: build/lib/core/models.py:778 core/models.py:778
|
||||
msgid "Make a new request"
|
||||
msgstr "Faire une nouvelle demande"
|
||||
|
||||
#: build/lib/core/models.py:879 core/models.py:879
|
||||
#: build/lib/core/models.py:877 core/models.py:877
|
||||
msgid "title"
|
||||
msgstr "titre"
|
||||
|
||||
#: build/lib/core/models.py:880 core/models.py:880
|
||||
#: build/lib/core/models.py:878 core/models.py:878
|
||||
msgid "excerpt"
|
||||
msgstr "extrait"
|
||||
|
||||
#: build/lib/core/models.py:929 core/models.py:929
|
||||
#: build/lib/core/models.py:927 core/models.py:927
|
||||
msgid "Document"
|
||||
msgstr "Document"
|
||||
|
||||
#: build/lib/core/models.py:930 core/models.py:930
|
||||
#: build/lib/core/models.py:928 core/models.py:928
|
||||
msgid "Documents"
|
||||
msgstr "Documents"
|
||||
|
||||
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
|
||||
#: core/models.py:942 core/models.py:1346
|
||||
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
|
||||
#: core/models.py:940 core/models.py:1345
|
||||
msgid "Untitled Document"
|
||||
msgstr "Document sans titre"
|
||||
|
||||
#: build/lib/core/models.py:1347 core/models.py:1347
|
||||
#: build/lib/core/models.py:1346 core/models.py:1346
|
||||
msgid "Open"
|
||||
msgstr "Ouvrir"
|
||||
|
||||
#: build/lib/core/models.py:1382 core/models.py:1382
|
||||
#: build/lib/core/models.py:1381 core/models.py:1381
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} a partagé un document avec vous!"
|
||||
|
||||
#: build/lib/core/models.py:1386 core/models.py:1386
|
||||
#: build/lib/core/models.py:1385 core/models.py:1385
|
||||
#, 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:1392 core/models.py:1392
|
||||
#: build/lib/core/models.py:1391 core/models.py:1391
|
||||
#, 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:1493 core/models.py:1493
|
||||
#: build/lib/core/models.py:1492 core/models.py:1492
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Trace du lien document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1494 core/models.py:1494
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Traces du lien document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1500 core/models.py:1500
|
||||
#: build/lib/core/models.py:1499 core/models.py:1499
|
||||
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:1523 core/models.py:1523
|
||||
#: build/lib/core/models.py:1522 core/models.py:1522
|
||||
msgid "Document favorite"
|
||||
msgstr "Document favori"
|
||||
|
||||
#: build/lib/core/models.py:1524 core/models.py:1524
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
msgid "Document favorites"
|
||||
msgstr "Documents favoris"
|
||||
|
||||
#: build/lib/core/models.py:1530 core/models.py:1530
|
||||
#: build/lib/core/models.py:1529 core/models.py:1529
|
||||
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:1552 core/models.py:1552
|
||||
#: build/lib/core/models.py:1551 core/models.py:1551
|
||||
msgid "Document/user relation"
|
||||
msgstr "Relation document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1553 core/models.py:1553
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
msgid "Document/user relations"
|
||||
msgstr "Relations document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1559 core/models.py:1559
|
||||
#: build/lib/core/models.py:1558 core/models.py:1558
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Cet utilisateur est déjà dans ce document."
|
||||
|
||||
#: build/lib/core/models.py:1565 core/models.py:1565
|
||||
#: build/lib/core/models.py:1564 core/models.py:1564
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Cette équipe est déjà dans ce document."
|
||||
|
||||
#: build/lib/core/models.py:1571 core/models.py:1571
|
||||
#: build/lib/core/models.py:1570 core/models.py:1570
|
||||
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:1722 core/models.py:1722
|
||||
#: build/lib/core/models.py:1721 core/models.py:1721
|
||||
msgid "Document ask for access"
|
||||
msgstr "Demande d'accès au document"
|
||||
|
||||
#: build/lib/core/models.py:1723 core/models.py:1723
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Demande d'accès au document"
|
||||
|
||||
#: build/lib/core/models.py:1729 core/models.py:1729
|
||||
#: build/lib/core/models.py:1728 core/models.py:1728
|
||||
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:1786 core/models.py:1786
|
||||
#: build/lib/core/models.py:1785 core/models.py:1785
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} souhaiterait accéder au document suivant !"
|
||||
|
||||
#: build/lib/core/models.py:1790 core/models.py:1790
|
||||
#: build/lib/core/models.py:1789 core/models.py:1789
|
||||
#, 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:1796 core/models.py:1796
|
||||
#: build/lib/core/models.py:1795 core/models.py:1795
|
||||
#, 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:1838 core/models.py:1838
|
||||
#: build/lib/core/models.py:1837 core/models.py:1837
|
||||
msgid "Thread"
|
||||
msgstr "Conversation"
|
||||
|
||||
#: build/lib/core/models.py:1839 core/models.py:1839
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
msgid "Threads"
|
||||
msgstr "Conversations"
|
||||
|
||||
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
|
||||
#: core/models.py:1842 core/models.py:1894
|
||||
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
|
||||
#: core/models.py:1841 core/models.py:1893
|
||||
msgid "Anonymous"
|
||||
msgstr "Anonyme"
|
||||
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
#: build/lib/core/models.py:1888 core/models.py:1888
|
||||
msgid "Comment"
|
||||
msgstr "Commentaire"
|
||||
|
||||
#: build/lib/core/models.py:1890 core/models.py:1890
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
msgid "Comments"
|
||||
msgstr "Commentaires"
|
||||
|
||||
#: build/lib/core/models.py:1939 core/models.py:1939
|
||||
#: build/lib/core/models.py:1938 core/models.py:1938
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr "Cet émoji a déjà été réagi à ce commentaire."
|
||||
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
#: build/lib/core/models.py:1942 core/models.py:1942
|
||||
msgid "Reaction"
|
||||
msgstr "Réaction"
|
||||
|
||||
#: build/lib/core/models.py:1944 core/models.py:1944
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
msgid "Reactions"
|
||||
msgstr "Réactions"
|
||||
|
||||
#: build/lib/core/models.py:1954 core/models.py:1954
|
||||
#: build/lib/core/models.py:1953 core/models.py:1953
|
||||
msgid "email address"
|
||||
msgstr "adresse e-mail"
|
||||
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
#: build/lib/core/models.py:1972 core/models.py:1972
|
||||
msgid "Document invitation"
|
||||
msgstr "Invitation à un document"
|
||||
|
||||
#: build/lib/core/models.py:1974 core/models.py:1974
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
msgid "Document invitations"
|
||||
msgstr "Invitations à un document"
|
||||
|
||||
#: build/lib/core/models.py:1994 core/models.py:1994
|
||||
#: build/lib/core/models.py:1993 core/models.py:1993
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
|
||||
|
||||
#: build/lib/impress/settings.py:702 impress/settings.py:702
|
||||
#: build/lib/impress/settings.py:808 impress/settings.py:808
|
||||
msgid "Docs AI"
|
||||
msgstr "Docs IA"
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
|
||||
"PO-Revision-Date: 2026-03-13 16:53\n"
|
||||
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 16:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Italian\n"
|
||||
"Language: it_IT\n"
|
||||
@@ -46,36 +46,40 @@ msgstr "Struttura ad albero"
|
||||
msgid "Title"
|
||||
msgstr "Titolo"
|
||||
|
||||
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
|
||||
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Creator is me"
|
||||
msgstr "Il creatore sono io"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
|
||||
msgid "Favorite"
|
||||
msgstr "Preferiti"
|
||||
|
||||
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
|
||||
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Un nuovo documento è stato creato a tuo nome!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
|
||||
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Sei ora proprietario di un nuovo documento:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
|
||||
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
|
||||
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copia di {title}"
|
||||
@@ -247,98 +251,98 @@ msgstr "utente"
|
||||
msgid "users"
|
||||
msgstr "utenti"
|
||||
|
||||
#: build/lib/core/models.py:378 core/models.py:378
|
||||
#: build/lib/core/models.py:376 core/models.py:376
|
||||
msgid "Active email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:379 core/models.py:379
|
||||
#: build/lib/core/models.py:377 core/models.py:377
|
||||
msgid "Email address to deactivate"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:406 core/models.py:406
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "Unique ID in the source file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
|
||||
#: core/models.py:708
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:413 core/models.py:413
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
|
||||
#: core/models.py:712
|
||||
msgid "Done"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
|
||||
#: core/models.py:713
|
||||
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
|
||||
#: core/models.py:711
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:423 core/models.py:423
|
||||
#: build/lib/core/models.py:421 core/models.py:421
|
||||
msgid "user reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 core/models.py:424
|
||||
#: build/lib/core/models.py:422 core/models.py:422
|
||||
msgid "user reconciliations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:662 core/models.py:662
|
||||
#: build/lib/core/models.py:660 core/models.py:660
|
||||
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
|
||||
" To confirm that you are the one who initiated the request\n"
|
||||
" and that this email belongs to you:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:668 core/models.py:668
|
||||
#: build/lib/core/models.py:666 core/models.py:666
|
||||
msgid "Confirm by clicking the link to start the reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
|
||||
#: core/models.py:779
|
||||
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
|
||||
#: core/models.py:777
|
||||
msgid "Click here"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:674 core/models.py:674
|
||||
#: build/lib/core/models.py:672 core/models.py:672
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:685 core/models.py:685
|
||||
#: build/lib/core/models.py:683 core/models.py:683
|
||||
msgid "Your reconciliation request has been processed.\n"
|
||||
" New documents are likely associated with your account:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:690 core/models.py:690
|
||||
#: build/lib/core/models.py:688 core/models.py:688
|
||||
msgid "Your accounts have been merged"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:695 core/models.py:695
|
||||
#: build/lib/core/models.py:693 core/models.py:693
|
||||
msgid "Click here to see"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:696 core/models.py:696
|
||||
#: build/lib/core/models.py:694 core/models.py:694
|
||||
msgid "See my documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:706 core/models.py:706
|
||||
#: build/lib/core/models.py:704 core/models.py:704
|
||||
msgid "CSV file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:711 core/models.py:711
|
||||
#: build/lib/core/models.py:709 core/models.py:709
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:721 core/models.py:721
|
||||
#: build/lib/core/models.py:719 core/models.py:719
|
||||
msgid "user reconciliation CSV import"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:722 core/models.py:722
|
||||
#: build/lib/core/models.py:720 core/models.py:720
|
||||
msgid "user reconciliation CSV imports"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:766 core/models.py:766
|
||||
#: build/lib/core/models.py:764 core/models.py:764
|
||||
#, python-brace-format
|
||||
msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" Reconciliation failed for the following email addresses:\n"
|
||||
@@ -347,175 +351,175 @@ msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" You can submit another request with the valid email addresses."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:774 core/models.py:774
|
||||
#: build/lib/core/models.py:772 core/models.py:772
|
||||
msgid "Reconciliation of your Docs accounts not completed"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:780 core/models.py:780
|
||||
#: build/lib/core/models.py:778 core/models.py:778
|
||||
msgid "Make a new request"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:879 core/models.py:879
|
||||
#: build/lib/core/models.py:877 core/models.py:877
|
||||
msgid "title"
|
||||
msgstr "titolo"
|
||||
|
||||
#: build/lib/core/models.py:880 core/models.py:880
|
||||
#: build/lib/core/models.py:878 core/models.py:878
|
||||
msgid "excerpt"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:929 core/models.py:929
|
||||
#: build/lib/core/models.py:927 core/models.py:927
|
||||
msgid "Document"
|
||||
msgstr "Documento"
|
||||
|
||||
#: build/lib/core/models.py:930 core/models.py:930
|
||||
#: build/lib/core/models.py:928 core/models.py:928
|
||||
msgid "Documents"
|
||||
msgstr "Documenti"
|
||||
|
||||
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
|
||||
#: core/models.py:942 core/models.py:1346
|
||||
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
|
||||
#: core/models.py:940 core/models.py:1345
|
||||
msgid "Untitled Document"
|
||||
msgstr "Documento senza titolo"
|
||||
|
||||
#: build/lib/core/models.py:1347 core/models.py:1347
|
||||
#: build/lib/core/models.py:1346 core/models.py:1346
|
||||
msgid "Open"
|
||||
msgstr "Apri"
|
||||
|
||||
#: build/lib/core/models.py:1382 core/models.py:1382
|
||||
#: build/lib/core/models.py:1381 core/models.py:1381
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} ha condiviso un documento con te!"
|
||||
|
||||
#: build/lib/core/models.py:1386 core/models.py:1386
|
||||
#: build/lib/core/models.py:1385 core/models.py:1385
|
||||
#, 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:1392 core/models.py:1392
|
||||
#: build/lib/core/models.py:1391 core/models.py:1391
|
||||
#, 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:1493 core/models.py:1493
|
||||
#: build/lib/core/models.py:1492 core/models.py:1492
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1494 core/models.py:1494
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1500 core/models.py:1500
|
||||
#: build/lib/core/models.py:1499 core/models.py:1499
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
#: build/lib/core/models.py:1522 core/models.py:1522
|
||||
msgid "Document favorite"
|
||||
msgstr "Documento preferito"
|
||||
|
||||
#: build/lib/core/models.py:1524 core/models.py:1524
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
msgid "Document favorites"
|
||||
msgstr "Documenti preferiti"
|
||||
|
||||
#: build/lib/core/models.py:1530 core/models.py:1530
|
||||
#: build/lib/core/models.py:1529 core/models.py:1529
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
#: build/lib/core/models.py:1551 core/models.py:1551
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1553 core/models.py:1553
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1559 core/models.py:1559
|
||||
#: build/lib/core/models.py:1558 core/models.py:1558
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Questo utente è già presente in questo documento."
|
||||
|
||||
#: build/lib/core/models.py:1565 core/models.py:1565
|
||||
#: build/lib/core/models.py:1564 core/models.py:1564
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Questo team è già presente in questo documento."
|
||||
|
||||
#: build/lib/core/models.py:1571 core/models.py:1571
|
||||
#: build/lib/core/models.py:1570 core/models.py:1570
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
#: build/lib/core/models.py:1721 core/models.py:1721
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1723 core/models.py:1723
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1729 core/models.py:1729
|
||||
#: build/lib/core/models.py:1728 core/models.py:1728
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1786 core/models.py:1786
|
||||
#: build/lib/core/models.py:1785 core/models.py:1785
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1790 core/models.py:1790
|
||||
#: build/lib/core/models.py:1789 core/models.py:1789
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1796 core/models.py:1796
|
||||
#: build/lib/core/models.py:1795 core/models.py:1795
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
#: build/lib/core/models.py:1837 core/models.py:1837
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1839 core/models.py:1839
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
|
||||
#: core/models.py:1842 core/models.py:1894
|
||||
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
|
||||
#: core/models.py:1841 core/models.py:1893
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
#: build/lib/core/models.py:1888 core/models.py:1888
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1890 core/models.py:1890
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1939 core/models.py:1939
|
||||
#: build/lib/core/models.py:1938 core/models.py:1938
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
#: build/lib/core/models.py:1942 core/models.py:1942
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1944 core/models.py:1944
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1954 core/models.py:1954
|
||||
#: build/lib/core/models.py:1953 core/models.py:1953
|
||||
msgid "email address"
|
||||
msgstr "indirizzo e-mail"
|
||||
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
#: build/lib/core/models.py:1972 core/models.py:1972
|
||||
msgid "Document invitation"
|
||||
msgstr "Invito al documento"
|
||||
|
||||
#: build/lib/core/models.py:1974 core/models.py:1974
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
msgid "Document invitations"
|
||||
msgstr "Inviti al documento"
|
||||
|
||||
#: build/lib/core/models.py:1994 core/models.py:1994
|
||||
#: build/lib/core/models.py:1993 core/models.py:1993
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Questa email è già associata a un utente registrato."
|
||||
|
||||
#: build/lib/impress/settings.py:702 impress/settings.py:702
|
||||
#: build/lib/impress/settings.py:808 impress/settings.py:808
|
||||
msgid "Docs AI"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
|
||||
"PO-Revision-Date: 2026-03-13 16:53\n"
|
||||
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 16:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Language: nl_NL\n"
|
||||
@@ -46,36 +46,40 @@ msgstr "Boomstructuur"
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
|
||||
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Creator is me"
|
||||
msgstr "Ik ben eigenaar"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
msgid "Masked"
|
||||
msgstr "Gemaskeerd"
|
||||
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
|
||||
msgid "Favorite"
|
||||
msgstr "Favoriet"
|
||||
|
||||
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
|
||||
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Een nieuw document is namens u gemaakt!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
|
||||
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "U heeft eigenaarschap van een nieuw document gekregen:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
|
||||
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
|
||||
msgid "This field is required."
|
||||
msgstr "Dit veld is verplicht."
|
||||
|
||||
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr "Link bereik '%(link_reach)s' is niet toegestaan op basis van bovenliggende documentconfiguratie."
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
|
||||
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "kopie van {title}"
|
||||
@@ -247,46 +251,46 @@ msgstr "gebruiker"
|
||||
msgid "users"
|
||||
msgstr "gebruikers"
|
||||
|
||||
#: build/lib/core/models.py:378 core/models.py:378
|
||||
#: build/lib/core/models.py:376 core/models.py:376
|
||||
msgid "Active email address"
|
||||
msgstr "Actieve e-mail adres"
|
||||
|
||||
#: build/lib/core/models.py:379 core/models.py:379
|
||||
#: build/lib/core/models.py:377 core/models.py:377
|
||||
msgid "Email address to deactivate"
|
||||
msgstr "E-mailadres om te deactiveren"
|
||||
|
||||
#: build/lib/core/models.py:406 core/models.py:406
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "Unique ID in the source file"
|
||||
msgstr "Unieke ID in het bronbestand"
|
||||
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
|
||||
#: core/models.py:708
|
||||
msgid "Pending"
|
||||
msgstr "In behandeling"
|
||||
|
||||
#: build/lib/core/models.py:413 core/models.py:413
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Ready"
|
||||
msgstr "Klaar"
|
||||
|
||||
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
|
||||
#: core/models.py:712
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
msgid "Done"
|
||||
msgstr "Klaar"
|
||||
|
||||
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
|
||||
#: core/models.py:713
|
||||
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
|
||||
#: core/models.py:711
|
||||
msgid "Error"
|
||||
msgstr "Fout"
|
||||
|
||||
#: build/lib/core/models.py:423 core/models.py:423
|
||||
#: build/lib/core/models.py:421 core/models.py:421
|
||||
msgid "user reconciliation"
|
||||
msgstr "gebruiker samenvoegen"
|
||||
|
||||
#: build/lib/core/models.py:424 core/models.py:424
|
||||
#: build/lib/core/models.py:422 core/models.py:422
|
||||
msgid "user reconciliations"
|
||||
msgstr "gebruikers samenvoegen"
|
||||
|
||||
#: build/lib/core/models.py:662 core/models.py:662
|
||||
#: build/lib/core/models.py:660 core/models.py:660
|
||||
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
|
||||
" To confirm that you are the one who initiated the request\n"
|
||||
" and that this email belongs to you:"
|
||||
@@ -294,54 +298,54 @@ msgstr "Je hebt gevraagd om een samenvoeging van je gebruikersaccounts op Docs.\
|
||||
" Om te bevestigen dat u degene bent die het verzoek\n"
|
||||
" heeft geïnitieerd en dat deze e-mail van u is:"
|
||||
|
||||
#: build/lib/core/models.py:668 core/models.py:668
|
||||
#: build/lib/core/models.py:666 core/models.py:666
|
||||
msgid "Confirm by clicking the link to start the reconciliation"
|
||||
msgstr "Bevestig door te klikken op de link om de samenvoeging te starten"
|
||||
|
||||
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
|
||||
#: core/models.py:779
|
||||
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
|
||||
#: core/models.py:777
|
||||
msgid "Click here"
|
||||
msgstr "Klik hier"
|
||||
|
||||
#: build/lib/core/models.py:674 core/models.py:674
|
||||
#: build/lib/core/models.py:672 core/models.py:672
|
||||
msgid "Confirm"
|
||||
msgstr "Bevestig"
|
||||
|
||||
#: build/lib/core/models.py:685 core/models.py:685
|
||||
#: build/lib/core/models.py:683 core/models.py:683
|
||||
msgid "Your reconciliation request has been processed.\n"
|
||||
" New documents are likely associated with your account:"
|
||||
msgstr "Uw samenvoegingsverzoek is verwerkt.\n"
|
||||
" Nieuwe documenten worden waarschijnlijk geassocieerd met uw account:"
|
||||
|
||||
#: build/lib/core/models.py:690 core/models.py:690
|
||||
#: build/lib/core/models.py:688 core/models.py:688
|
||||
msgid "Your accounts have been merged"
|
||||
msgstr "Je accounts zijn samengevoegd"
|
||||
|
||||
#: build/lib/core/models.py:695 core/models.py:695
|
||||
#: build/lib/core/models.py:693 core/models.py:693
|
||||
msgid "Click here to see"
|
||||
msgstr "Klik hier om te bekijken"
|
||||
|
||||
#: build/lib/core/models.py:696 core/models.py:696
|
||||
#: build/lib/core/models.py:694 core/models.py:694
|
||||
msgid "See my documents"
|
||||
msgstr "Mijn documenten bekijken"
|
||||
|
||||
#: build/lib/core/models.py:706 core/models.py:706
|
||||
#: build/lib/core/models.py:704 core/models.py:704
|
||||
msgid "CSV file"
|
||||
msgstr "CSV bestand"
|
||||
|
||||
#: build/lib/core/models.py:711 core/models.py:711
|
||||
#: build/lib/core/models.py:709 core/models.py:709
|
||||
msgid "Running"
|
||||
msgstr "Bezig"
|
||||
|
||||
#: build/lib/core/models.py:721 core/models.py:721
|
||||
#: build/lib/core/models.py:719 core/models.py:719
|
||||
msgid "user reconciliation CSV import"
|
||||
msgstr "gebruiker samenvoeging CSV import"
|
||||
|
||||
#: build/lib/core/models.py:722 core/models.py:722
|
||||
#: build/lib/core/models.py:720 core/models.py:720
|
||||
msgid "user reconciliation CSV imports"
|
||||
msgstr "gebruiker reconciliation CSV imports"
|
||||
|
||||
#: build/lib/core/models.py:766 core/models.py:766
|
||||
#: build/lib/core/models.py:764 core/models.py:764
|
||||
#, python-brace-format
|
||||
msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" Reconciliation failed for the following email addresses:\n"
|
||||
@@ -354,175 +358,175 @@ msgstr "Uw verzoek tot verzoening is mislukt.\n"
|
||||
" Controleer op typefouten.\n"
|
||||
" U kunt een ander verzoek indienen met de geldige e-mailadressen."
|
||||
|
||||
#: build/lib/core/models.py:774 core/models.py:774
|
||||
#: build/lib/core/models.py:772 core/models.py:772
|
||||
msgid "Reconciliation of your Docs accounts not completed"
|
||||
msgstr "Samenvoeging van je Docs accounts is niet voltooid"
|
||||
|
||||
#: build/lib/core/models.py:780 core/models.py:780
|
||||
#: build/lib/core/models.py:778 core/models.py:778
|
||||
msgid "Make a new request"
|
||||
msgstr "Maak een nieuw verzoek"
|
||||
|
||||
#: build/lib/core/models.py:879 core/models.py:879
|
||||
#: build/lib/core/models.py:877 core/models.py:877
|
||||
msgid "title"
|
||||
msgstr "titel"
|
||||
|
||||
#: build/lib/core/models.py:880 core/models.py:880
|
||||
#: build/lib/core/models.py:878 core/models.py:878
|
||||
msgid "excerpt"
|
||||
msgstr "uittreksel"
|
||||
|
||||
#: build/lib/core/models.py:929 core/models.py:929
|
||||
#: build/lib/core/models.py:927 core/models.py:927
|
||||
msgid "Document"
|
||||
msgstr "Document"
|
||||
|
||||
#: build/lib/core/models.py:930 core/models.py:930
|
||||
#: build/lib/core/models.py:928 core/models.py:928
|
||||
msgid "Documents"
|
||||
msgstr "Documenten"
|
||||
|
||||
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
|
||||
#: core/models.py:942 core/models.py:1346
|
||||
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
|
||||
#: core/models.py:940 core/models.py:1345
|
||||
msgid "Untitled Document"
|
||||
msgstr "Naamloos Document"
|
||||
|
||||
#: build/lib/core/models.py:1347 core/models.py:1347
|
||||
#: build/lib/core/models.py:1346 core/models.py:1346
|
||||
msgid "Open"
|
||||
msgstr "Open"
|
||||
|
||||
#: build/lib/core/models.py:1382 core/models.py:1382
|
||||
#: build/lib/core/models.py:1381 core/models.py:1381
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} heeft een document met u gedeeld!"
|
||||
|
||||
#: build/lib/core/models.py:1386 core/models.py:1386
|
||||
#: build/lib/core/models.py:1385 core/models.py:1385
|
||||
#, 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:1392 core/models.py:1392
|
||||
#: build/lib/core/models.py:1391 core/models.py:1391
|
||||
#, 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:1493 core/models.py:1493
|
||||
#: build/lib/core/models.py:1492 core/models.py:1492
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Document/gebruiker link"
|
||||
|
||||
#: build/lib/core/models.py:1494 core/models.py:1494
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Document/gebruiker link"
|
||||
|
||||
#: build/lib/core/models.py:1500 core/models.py:1500
|
||||
#: build/lib/core/models.py:1499 core/models.py:1499
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Een link bestaat al voor dit document/deze gebruiker."
|
||||
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
#: build/lib/core/models.py:1522 core/models.py:1522
|
||||
msgid "Document favorite"
|
||||
msgstr "Document favoriet"
|
||||
|
||||
#: build/lib/core/models.py:1524 core/models.py:1524
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
msgid "Document favorites"
|
||||
msgstr "Document favorieten"
|
||||
|
||||
#: build/lib/core/models.py:1530 core/models.py:1530
|
||||
#: build/lib/core/models.py:1529 core/models.py:1529
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Dit document is al in gebruik als favoriet door dezelfde gebruiker."
|
||||
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
#: build/lib/core/models.py:1551 core/models.py:1551
|
||||
msgid "Document/user relation"
|
||||
msgstr "Document/gebruiker relatie"
|
||||
|
||||
#: build/lib/core/models.py:1553 core/models.py:1553
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
msgid "Document/user relations"
|
||||
msgstr "Document/gebruiker relaties"
|
||||
|
||||
#: build/lib/core/models.py:1559 core/models.py:1559
|
||||
#: build/lib/core/models.py:1558 core/models.py:1558
|
||||
msgid "This user is already in this document."
|
||||
msgstr "De gebruiker bestaat al in dit document."
|
||||
|
||||
#: build/lib/core/models.py:1565 core/models.py:1565
|
||||
#: build/lib/core/models.py:1564 core/models.py:1564
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Dit team bestaat al in dit document."
|
||||
|
||||
#: build/lib/core/models.py:1571 core/models.py:1571
|
||||
#: build/lib/core/models.py:1570 core/models.py:1570
|
||||
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:1722 core/models.py:1722
|
||||
#: build/lib/core/models.py:1721 core/models.py:1721
|
||||
msgid "Document ask for access"
|
||||
msgstr "Document verzoekt om toegang"
|
||||
|
||||
#: build/lib/core/models.py:1723 core/models.py:1723
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Document verzoekt om toegangen"
|
||||
|
||||
#: build/lib/core/models.py:1729 core/models.py:1729
|
||||
#: build/lib/core/models.py:1728 core/models.py:1728
|
||||
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:1786 core/models.py:1786
|
||||
#: build/lib/core/models.py:1785 core/models.py:1785
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} verzoekt toegang tot een document!"
|
||||
|
||||
#: build/lib/core/models.py:1790 core/models.py:1790
|
||||
#: build/lib/core/models.py:1789 core/models.py:1789
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} verzoekt toegang tot het volgende document:"
|
||||
|
||||
#: build/lib/core/models.py:1796 core/models.py:1796
|
||||
#: build/lib/core/models.py:1795 core/models.py:1795
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name} verzoekt toegang tot het document: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
#: build/lib/core/models.py:1837 core/models.py:1837
|
||||
msgid "Thread"
|
||||
msgstr "Kanaal"
|
||||
|
||||
#: build/lib/core/models.py:1839 core/models.py:1839
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
msgid "Threads"
|
||||
msgstr "Kanalen"
|
||||
|
||||
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
|
||||
#: core/models.py:1842 core/models.py:1894
|
||||
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
|
||||
#: core/models.py:1841 core/models.py:1893
|
||||
msgid "Anonymous"
|
||||
msgstr "Anoniem"
|
||||
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
#: build/lib/core/models.py:1888 core/models.py:1888
|
||||
msgid "Comment"
|
||||
msgstr "Reactie"
|
||||
|
||||
#: build/lib/core/models.py:1890 core/models.py:1890
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
msgid "Comments"
|
||||
msgstr "Reacties"
|
||||
|
||||
#: build/lib/core/models.py:1939 core/models.py:1939
|
||||
#: build/lib/core/models.py:1938 core/models.py:1938
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr "Deze emoji is al op deze opmerking gereageerd."
|
||||
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
#: build/lib/core/models.py:1942 core/models.py:1942
|
||||
msgid "Reaction"
|
||||
msgstr "Reactie"
|
||||
|
||||
#: build/lib/core/models.py:1944 core/models.py:1944
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
msgid "Reactions"
|
||||
msgstr "Reacties"
|
||||
|
||||
#: build/lib/core/models.py:1954 core/models.py:1954
|
||||
#: build/lib/core/models.py:1953 core/models.py:1953
|
||||
msgid "email address"
|
||||
msgstr "e-mailadres"
|
||||
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
#: build/lib/core/models.py:1972 core/models.py:1972
|
||||
msgid "Document invitation"
|
||||
msgstr "Document uitnodiging"
|
||||
|
||||
#: build/lib/core/models.py:1974 core/models.py:1974
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
msgid "Document invitations"
|
||||
msgstr "Document uitnodigingen"
|
||||
|
||||
#: build/lib/core/models.py:1994 core/models.py:1994
|
||||
#: build/lib/core/models.py:1993 core/models.py:1993
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
|
||||
|
||||
#: build/lib/impress/settings.py:702 impress/settings.py:702
|
||||
#: build/lib/impress/settings.py:808 impress/settings.py:808
|
||||
msgid "Docs AI"
|
||||
msgstr "Docs AI"
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
|
||||
"PO-Revision-Date: 2026-03-13 16:53\n"
|
||||
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 16:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Portuguese\n"
|
||||
"Language: pt_PT\n"
|
||||
@@ -46,36 +46,40 @@ msgstr "Estrutura de árvore"
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
|
||||
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Creator is me"
|
||||
msgstr "Eu sou o criador"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
|
||||
msgid "Favorite"
|
||||
msgstr "Favorito"
|
||||
|
||||
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
|
||||
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Um novo documento foi criado em seu nome!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
|
||||
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "A propriedade de um novo documento foi concedida a você:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
|
||||
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
|
||||
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "cópia de {title}"
|
||||
@@ -137,7 +141,7 @@ msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
msgid "Left"
|
||||
msgstr ""
|
||||
msgstr "Esquerda"
|
||||
|
||||
#: build/lib/core/enums.py:41 core/enums.py:41
|
||||
msgid "Right"
|
||||
@@ -145,7 +149,7 @@ msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:80 core/models.py:80
|
||||
msgid "id"
|
||||
msgstr ""
|
||||
msgstr "id"
|
||||
|
||||
#: build/lib/core/models.py:81 core/models.py:81
|
||||
msgid "primary key for the record as UUID"
|
||||
@@ -173,7 +177,7 @@ msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:141 core/models.py:141
|
||||
msgid "sub"
|
||||
msgstr ""
|
||||
msgstr "sub"
|
||||
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Required. 255 characters or fewer. ASCII characters only."
|
||||
@@ -241,104 +245,104 @@ msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:209 core/models.py:209
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
msgstr "utilizador"
|
||||
|
||||
#: build/lib/core/models.py:210 core/models.py:210
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:378 core/models.py:378
|
||||
#: build/lib/core/models.py:376 core/models.py:376
|
||||
msgid "Active email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:379 core/models.py:379
|
||||
#: build/lib/core/models.py:377 core/models.py:377
|
||||
msgid "Email address to deactivate"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:406 core/models.py:406
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "Unique ID in the source file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
|
||||
#: core/models.py:708
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:413 core/models.py:413
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
|
||||
#: core/models.py:712
|
||||
msgid "Done"
|
||||
msgstr ""
|
||||
msgstr "Concluído"
|
||||
|
||||
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
|
||||
#: core/models.py:713
|
||||
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
|
||||
#: core/models.py:711
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:423 core/models.py:423
|
||||
#: build/lib/core/models.py:421 core/models.py:421
|
||||
msgid "user reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 core/models.py:424
|
||||
#: build/lib/core/models.py:422 core/models.py:422
|
||||
msgid "user reconciliations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:662 core/models.py:662
|
||||
#: build/lib/core/models.py:660 core/models.py:660
|
||||
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
|
||||
" To confirm that you are the one who initiated the request\n"
|
||||
" and that this email belongs to you:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:668 core/models.py:668
|
||||
#: build/lib/core/models.py:666 core/models.py:666
|
||||
msgid "Confirm by clicking the link to start the reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
|
||||
#: core/models.py:779
|
||||
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
|
||||
#: core/models.py:777
|
||||
msgid "Click here"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:674 core/models.py:674
|
||||
#: build/lib/core/models.py:672 core/models.py:672
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:685 core/models.py:685
|
||||
#: build/lib/core/models.py:683 core/models.py:683
|
||||
msgid "Your reconciliation request has been processed.\n"
|
||||
" New documents are likely associated with your account:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:690 core/models.py:690
|
||||
#: build/lib/core/models.py:688 core/models.py:688
|
||||
msgid "Your accounts have been merged"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:695 core/models.py:695
|
||||
#: build/lib/core/models.py:693 core/models.py:693
|
||||
msgid "Click here to see"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:696 core/models.py:696
|
||||
#: build/lib/core/models.py:694 core/models.py:694
|
||||
msgid "See my documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:706 core/models.py:706
|
||||
#: build/lib/core/models.py:704 core/models.py:704
|
||||
msgid "CSV file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:711 core/models.py:711
|
||||
#: build/lib/core/models.py:709 core/models.py:709
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:721 core/models.py:721
|
||||
#: build/lib/core/models.py:719 core/models.py:719
|
||||
msgid "user reconciliation CSV import"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:722 core/models.py:722
|
||||
#: build/lib/core/models.py:720 core/models.py:720
|
||||
msgid "user reconciliation CSV imports"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:766 core/models.py:766
|
||||
#: build/lib/core/models.py:764 core/models.py:764
|
||||
#, python-brace-format
|
||||
msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" Reconciliation failed for the following email addresses:\n"
|
||||
@@ -347,175 +351,175 @@ msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" You can submit another request with the valid email addresses."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:774 core/models.py:774
|
||||
#: build/lib/core/models.py:772 core/models.py:772
|
||||
msgid "Reconciliation of your Docs accounts not completed"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:780 core/models.py:780
|
||||
#: build/lib/core/models.py:778 core/models.py:778
|
||||
msgid "Make a new request"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:879 core/models.py:879
|
||||
#: build/lib/core/models.py:877 core/models.py:877
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:880 core/models.py:880
|
||||
#: build/lib/core/models.py:878 core/models.py:878
|
||||
msgid "excerpt"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:929 core/models.py:929
|
||||
#: build/lib/core/models.py:927 core/models.py:927
|
||||
msgid "Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:930 core/models.py:930
|
||||
#: build/lib/core/models.py:928 core/models.py:928
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
|
||||
#: core/models.py:942 core/models.py:1346
|
||||
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
|
||||
#: core/models.py:940 core/models.py:1345
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1347 core/models.py:1347
|
||||
#: build/lib/core/models.py:1346 core/models.py:1346
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
msgstr "Abrir"
|
||||
|
||||
#: build/lib/core/models.py:1382 core/models.py:1382
|
||||
#: build/lib/core/models.py:1381 core/models.py:1381
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1386 core/models.py:1386
|
||||
#: build/lib/core/models.py:1385 core/models.py:1385
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1392 core/models.py:1392
|
||||
#: build/lib/core/models.py:1391 core/models.py:1391
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
#: build/lib/core/models.py:1492 core/models.py:1492
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1494 core/models.py:1494
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1500 core/models.py:1500
|
||||
#: build/lib/core/models.py:1499 core/models.py:1499
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
#: build/lib/core/models.py:1522 core/models.py:1522
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1524 core/models.py:1524
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1530 core/models.py:1530
|
||||
#: build/lib/core/models.py:1529 core/models.py:1529
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
#: build/lib/core/models.py:1551 core/models.py:1551
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1553 core/models.py:1553
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1559 core/models.py:1559
|
||||
#: build/lib/core/models.py:1558 core/models.py:1558
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1565 core/models.py:1565
|
||||
#: build/lib/core/models.py:1564 core/models.py:1564
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1571 core/models.py:1571
|
||||
#: build/lib/core/models.py:1570 core/models.py:1570
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
#: build/lib/core/models.py:1721 core/models.py:1721
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1723 core/models.py:1723
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1729 core/models.py:1729
|
||||
#: build/lib/core/models.py:1728 core/models.py:1728
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1786 core/models.py:1786
|
||||
#: build/lib/core/models.py:1785 core/models.py:1785
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1790 core/models.py:1790
|
||||
#: build/lib/core/models.py:1789 core/models.py:1789
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1796 core/models.py:1796
|
||||
#: build/lib/core/models.py:1795 core/models.py:1795
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
#: build/lib/core/models.py:1837 core/models.py:1837
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1839 core/models.py:1839
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
|
||||
#: core/models.py:1842 core/models.py:1894
|
||||
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
|
||||
#: core/models.py:1841 core/models.py:1893
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
#: build/lib/core/models.py:1888 core/models.py:1888
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1890 core/models.py:1890
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1939 core/models.py:1939
|
||||
#: build/lib/core/models.py:1938 core/models.py:1938
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
#: build/lib/core/models.py:1942 core/models.py:1942
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1944 core/models.py:1944
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1954 core/models.py:1954
|
||||
#: build/lib/core/models.py:1953 core/models.py:1953
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
#: build/lib/core/models.py:1972 core/models.py:1972
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1974 core/models.py:1974
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1994 core/models.py:1994
|
||||
#: build/lib/core/models.py:1993 core/models.py:1993
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/impress/settings.py:702 impress/settings.py:702
|
||||
#: build/lib/impress/settings.py:808 impress/settings.py:808
|
||||
msgid "Docs AI"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
|
||||
"PO-Revision-Date: 2026-03-13 16:53\n"
|
||||
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 16:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Russian\n"
|
||||
"Language: ru_RU\n"
|
||||
@@ -46,36 +46,40 @@ msgstr "Древовидная структура"
|
||||
msgid "Title"
|
||||
msgstr "Заголовок"
|
||||
|
||||
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
|
||||
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
|
||||
msgid "Search"
|
||||
msgstr "Поиск"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Creator is me"
|
||||
msgstr "Создатель - я"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
msgid "Masked"
|
||||
msgstr "Скрытый"
|
||||
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
|
||||
msgid "Favorite"
|
||||
msgstr "Избранное"
|
||||
|
||||
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
|
||||
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Новый документ был создан от вашего имени!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
|
||||
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Вы назначены владельцем для нового документа:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
|
||||
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
|
||||
msgid "This field is required."
|
||||
msgstr "Это поле обязательное."
|
||||
|
||||
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr "Доступ по ссылке '%(link_reach)s' запрещён в соответствии с настройками родительского документа."
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
|
||||
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "копия {title}"
|
||||
@@ -247,46 +251,46 @@ msgstr "пользователь"
|
||||
msgid "users"
|
||||
msgstr "пользователи"
|
||||
|
||||
#: build/lib/core/models.py:378 core/models.py:378
|
||||
#: build/lib/core/models.py:376 core/models.py:376
|
||||
msgid "Active email address"
|
||||
msgstr "Активный адрес электронной почты"
|
||||
|
||||
#: build/lib/core/models.py:379 core/models.py:379
|
||||
#: build/lib/core/models.py:377 core/models.py:377
|
||||
msgid "Email address to deactivate"
|
||||
msgstr "Адрес электронной почты для деактивации"
|
||||
|
||||
#: build/lib/core/models.py:406 core/models.py:406
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "Unique ID in the source file"
|
||||
msgstr "Уникальный идентификатор в исходном файле"
|
||||
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
|
||||
#: core/models.py:708
|
||||
msgid "Pending"
|
||||
msgstr "В обработке"
|
||||
|
||||
#: build/lib/core/models.py:413 core/models.py:413
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Ready"
|
||||
msgstr "Готово"
|
||||
|
||||
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
|
||||
#: core/models.py:712
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
msgid "Done"
|
||||
msgstr "Выполнено"
|
||||
|
||||
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
|
||||
#: core/models.py:713
|
||||
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
|
||||
#: core/models.py:711
|
||||
msgid "Error"
|
||||
msgstr "Ошибка"
|
||||
|
||||
#: build/lib/core/models.py:423 core/models.py:423
|
||||
#: build/lib/core/models.py:421 core/models.py:421
|
||||
msgid "user reconciliation"
|
||||
msgstr "сверка данных пользователя"
|
||||
|
||||
#: build/lib/core/models.py:424 core/models.py:424
|
||||
#: build/lib/core/models.py:422 core/models.py:422
|
||||
msgid "user reconciliations"
|
||||
msgstr "сверки данных пользователя"
|
||||
|
||||
#: build/lib/core/models.py:662 core/models.py:662
|
||||
#: build/lib/core/models.py:660 core/models.py:660
|
||||
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
|
||||
" To confirm that you are the one who initiated the request\n"
|
||||
" and that this email belongs to you:"
|
||||
@@ -294,54 +298,54 @@ msgstr "Вы запросили сверку учётных записей по
|
||||
" Чтобы подтвердить факт того, что вы являетесь инициатором запроса\n"
|
||||
" и что этот адрес принадлежит вам:"
|
||||
|
||||
#: build/lib/core/models.py:668 core/models.py:668
|
||||
#: build/lib/core/models.py:666 core/models.py:666
|
||||
msgid "Confirm by clicking the link to start the reconciliation"
|
||||
msgstr "Чтобы начать сверку, подтвердите это, нажав на ссылку"
|
||||
|
||||
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
|
||||
#: core/models.py:779
|
||||
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
|
||||
#: core/models.py:777
|
||||
msgid "Click here"
|
||||
msgstr "Нажмите здесь"
|
||||
|
||||
#: build/lib/core/models.py:674 core/models.py:674
|
||||
#: build/lib/core/models.py:672 core/models.py:672
|
||||
msgid "Confirm"
|
||||
msgstr "Подтверждение"
|
||||
|
||||
#: build/lib/core/models.py:685 core/models.py:685
|
||||
#: build/lib/core/models.py:683 core/models.py:683
|
||||
msgid "Your reconciliation request has been processed.\n"
|
||||
" New documents are likely associated with your account:"
|
||||
msgstr "Ваш запрос на сверку был обработан.\n"
|
||||
" Новые документы, вероятно, связаны с вашей учётной записью:"
|
||||
|
||||
#: build/lib/core/models.py:690 core/models.py:690
|
||||
#: build/lib/core/models.py:688 core/models.py:688
|
||||
msgid "Your accounts have been merged"
|
||||
msgstr "Ваши учётные записи были объединены"
|
||||
|
||||
#: build/lib/core/models.py:695 core/models.py:695
|
||||
#: build/lib/core/models.py:693 core/models.py:693
|
||||
msgid "Click here to see"
|
||||
msgstr "Нажмите здесь, чтобы просмотреть"
|
||||
|
||||
#: build/lib/core/models.py:696 core/models.py:696
|
||||
#: build/lib/core/models.py:694 core/models.py:694
|
||||
msgid "See my documents"
|
||||
msgstr "Просмотреть мои документы"
|
||||
|
||||
#: build/lib/core/models.py:706 core/models.py:706
|
||||
#: build/lib/core/models.py:704 core/models.py:704
|
||||
msgid "CSV file"
|
||||
msgstr "CSV-файл"
|
||||
|
||||
#: build/lib/core/models.py:711 core/models.py:711
|
||||
#: build/lib/core/models.py:709 core/models.py:709
|
||||
msgid "Running"
|
||||
msgstr "Выполнение"
|
||||
|
||||
#: build/lib/core/models.py:721 core/models.py:721
|
||||
#: build/lib/core/models.py:719 core/models.py:719
|
||||
msgid "user reconciliation CSV import"
|
||||
msgstr "импорт из CSV сверки пользователей"
|
||||
|
||||
#: build/lib/core/models.py:722 core/models.py:722
|
||||
#: build/lib/core/models.py:720 core/models.py:720
|
||||
msgid "user reconciliation CSV imports"
|
||||
msgstr "импорты из CSV сверки пользователями"
|
||||
|
||||
#: build/lib/core/models.py:766 core/models.py:766
|
||||
#: build/lib/core/models.py:764 core/models.py:764
|
||||
#, python-brace-format
|
||||
msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" Reconciliation failed for the following email addresses:\n"
|
||||
@@ -354,175 +358,175 @@ msgstr "Ваш запрос на сверку не удался.\n"
|
||||
" Пожалуйста, проверьте, нет ли в них опечаток.\n"
|
||||
" Вы можете отправить ещё один запрос с действительными адресами электронной почты."
|
||||
|
||||
#: build/lib/core/models.py:774 core/models.py:774
|
||||
#: build/lib/core/models.py:772 core/models.py:772
|
||||
msgid "Reconciliation of your Docs accounts not completed"
|
||||
msgstr "Сверка ваших учётных записей Docs не завершена"
|
||||
|
||||
#: build/lib/core/models.py:780 core/models.py:780
|
||||
#: build/lib/core/models.py:778 core/models.py:778
|
||||
msgid "Make a new request"
|
||||
msgstr "Создать новый запрос"
|
||||
|
||||
#: build/lib/core/models.py:879 core/models.py:879
|
||||
#: build/lib/core/models.py:877 core/models.py:877
|
||||
msgid "title"
|
||||
msgstr "заголовок"
|
||||
|
||||
#: build/lib/core/models.py:880 core/models.py:880
|
||||
#: build/lib/core/models.py:878 core/models.py:878
|
||||
msgid "excerpt"
|
||||
msgstr "отрывок"
|
||||
|
||||
#: build/lib/core/models.py:929 core/models.py:929
|
||||
#: build/lib/core/models.py:927 core/models.py:927
|
||||
msgid "Document"
|
||||
msgstr "Документ"
|
||||
|
||||
#: build/lib/core/models.py:930 core/models.py:930
|
||||
#: build/lib/core/models.py:928 core/models.py:928
|
||||
msgid "Documents"
|
||||
msgstr "Документы"
|
||||
|
||||
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
|
||||
#: core/models.py:942 core/models.py:1346
|
||||
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
|
||||
#: core/models.py:940 core/models.py:1345
|
||||
msgid "Untitled Document"
|
||||
msgstr "Безымянный документ"
|
||||
|
||||
#: build/lib/core/models.py:1347 core/models.py:1347
|
||||
#: build/lib/core/models.py:1346 core/models.py:1346
|
||||
msgid "Open"
|
||||
msgstr "Открыть"
|
||||
|
||||
#: build/lib/core/models.py:1382 core/models.py:1382
|
||||
#: build/lib/core/models.py:1381 core/models.py:1381
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} делится с вами документом!"
|
||||
|
||||
#: build/lib/core/models.py:1386 core/models.py:1386
|
||||
#: build/lib/core/models.py:1385 core/models.py:1385
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
|
||||
|
||||
#: build/lib/core/models.py:1392 core/models.py:1392
|
||||
#: build/lib/core/models.py:1391 core/models.py:1391
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} делится с вами документом: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
#: build/lib/core/models.py:1492 core/models.py:1492
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Трассировка связи документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1494 core/models.py:1494
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Трассировка связей документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1500 core/models.py:1500
|
||||
#: build/lib/core/models.py:1499 core/models.py:1499
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
|
||||
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
#: build/lib/core/models.py:1522 core/models.py:1522
|
||||
msgid "Document favorite"
|
||||
msgstr "Избранный документ"
|
||||
|
||||
#: build/lib/core/models.py:1524 core/models.py:1524
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
msgid "Document favorites"
|
||||
msgstr "Избранные документы"
|
||||
|
||||
#: build/lib/core/models.py:1530 core/models.py:1530
|
||||
#: build/lib/core/models.py:1529 core/models.py:1529
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Этот документ уже помечен как избранный для этого пользователя."
|
||||
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
#: build/lib/core/models.py:1551 core/models.py:1551
|
||||
msgid "Document/user relation"
|
||||
msgstr "Отношение документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1553 core/models.py:1553
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
msgid "Document/user relations"
|
||||
msgstr "Отношения документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1559 core/models.py:1559
|
||||
#: build/lib/core/models.py:1558 core/models.py:1558
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Этот пользователь уже имеет доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1565 core/models.py:1565
|
||||
#: build/lib/core/models.py:1564 core/models.py:1564
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Эта команда уже имеет доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1571 core/models.py:1571
|
||||
#: build/lib/core/models.py:1570 core/models.py:1570
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
|
||||
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
#: build/lib/core/models.py:1721 core/models.py:1721
|
||||
msgid "Document ask for access"
|
||||
msgstr "Документ запрашивает доступ"
|
||||
|
||||
#: build/lib/core/models.py:1723 core/models.py:1723
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Документ запрашивает доступы"
|
||||
|
||||
#: build/lib/core/models.py:1729 core/models.py:1729
|
||||
#: build/lib/core/models.py:1728 core/models.py:1728
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "Этот пользователь уже запросил доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1786 core/models.py:1786
|
||||
#: build/lib/core/models.py:1785 core/models.py:1785
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} хочет получить доступ к документу!"
|
||||
|
||||
#: build/lib/core/models.py:1790 core/models.py:1790
|
||||
#: build/lib/core/models.py:1789 core/models.py:1789
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} хочет получить доступ к следующему документу:"
|
||||
|
||||
#: build/lib/core/models.py:1796 core/models.py:1796
|
||||
#: build/lib/core/models.py:1795 core/models.py:1795
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name} запрашивает доступ к документу: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
#: build/lib/core/models.py:1837 core/models.py:1837
|
||||
msgid "Thread"
|
||||
msgstr "Обсуждение"
|
||||
|
||||
#: build/lib/core/models.py:1839 core/models.py:1839
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
msgid "Threads"
|
||||
msgstr "Обсуждения"
|
||||
|
||||
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
|
||||
#: core/models.py:1842 core/models.py:1894
|
||||
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
|
||||
#: core/models.py:1841 core/models.py:1893
|
||||
msgid "Anonymous"
|
||||
msgstr "Аноним"
|
||||
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
#: build/lib/core/models.py:1888 core/models.py:1888
|
||||
msgid "Comment"
|
||||
msgstr "Комментарий"
|
||||
|
||||
#: build/lib/core/models.py:1890 core/models.py:1890
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
msgid "Comments"
|
||||
msgstr "Комментарии"
|
||||
|
||||
#: build/lib/core/models.py:1939 core/models.py:1939
|
||||
#: build/lib/core/models.py:1938 core/models.py:1938
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr "Этот эмодзи уже использован в этом комментарии."
|
||||
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
#: build/lib/core/models.py:1942 core/models.py:1942
|
||||
msgid "Reaction"
|
||||
msgstr "Реакция"
|
||||
|
||||
#: build/lib/core/models.py:1944 core/models.py:1944
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
msgid "Reactions"
|
||||
msgstr "Реакции"
|
||||
|
||||
#: build/lib/core/models.py:1954 core/models.py:1954
|
||||
#: build/lib/core/models.py:1953 core/models.py:1953
|
||||
msgid "email address"
|
||||
msgstr "адрес электронной почты"
|
||||
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
#: build/lib/core/models.py:1972 core/models.py:1972
|
||||
msgid "Document invitation"
|
||||
msgstr "Приглашение для документа"
|
||||
|
||||
#: build/lib/core/models.py:1974 core/models.py:1974
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
msgid "Document invitations"
|
||||
msgstr "Приглашения для документов"
|
||||
|
||||
#: build/lib/core/models.py:1994 core/models.py:1994
|
||||
#: build/lib/core/models.py:1993 core/models.py:1993
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Этот адрес уже связан с зарегистрированным пользователем."
|
||||
|
||||
#: build/lib/impress/settings.py:702 impress/settings.py:702
|
||||
#: build/lib/impress/settings.py:808 impress/settings.py:808
|
||||
msgid "Docs AI"
|
||||
msgstr "Docs ИИ"
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
|
||||
"PO-Revision-Date: 2026-03-13 16:53\n"
|
||||
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 16:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Slovenian\n"
|
||||
"Language: sl_SI\n"
|
||||
@@ -46,36 +46,40 @@ msgstr "Drevesna struktura"
|
||||
msgid "Title"
|
||||
msgstr "Naslov"
|
||||
|
||||
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
|
||||
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Creator is me"
|
||||
msgstr "Ustvaril sem jaz"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
|
||||
msgid "Favorite"
|
||||
msgstr "Priljubljena"
|
||||
|
||||
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
|
||||
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Nov dokument je bil ustvarjen v vašem imenu!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
|
||||
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Dodeljeno vam je bilo lastništvo nad novim dokumentom:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
|
||||
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
|
||||
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
@@ -247,98 +251,98 @@ msgstr "uporabnik"
|
||||
msgid "users"
|
||||
msgstr "uporabniki"
|
||||
|
||||
#: build/lib/core/models.py:378 core/models.py:378
|
||||
#: build/lib/core/models.py:376 core/models.py:376
|
||||
msgid "Active email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:379 core/models.py:379
|
||||
#: build/lib/core/models.py:377 core/models.py:377
|
||||
msgid "Email address to deactivate"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:406 core/models.py:406
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "Unique ID in the source file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
|
||||
#: core/models.py:708
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:413 core/models.py:413
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
|
||||
#: core/models.py:712
|
||||
msgid "Done"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
|
||||
#: core/models.py:713
|
||||
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
|
||||
#: core/models.py:711
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:423 core/models.py:423
|
||||
#: build/lib/core/models.py:421 core/models.py:421
|
||||
msgid "user reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 core/models.py:424
|
||||
#: build/lib/core/models.py:422 core/models.py:422
|
||||
msgid "user reconciliations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:662 core/models.py:662
|
||||
#: build/lib/core/models.py:660 core/models.py:660
|
||||
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
|
||||
" To confirm that you are the one who initiated the request\n"
|
||||
" and that this email belongs to you:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:668 core/models.py:668
|
||||
#: build/lib/core/models.py:666 core/models.py:666
|
||||
msgid "Confirm by clicking the link to start the reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
|
||||
#: core/models.py:779
|
||||
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
|
||||
#: core/models.py:777
|
||||
msgid "Click here"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:674 core/models.py:674
|
||||
#: build/lib/core/models.py:672 core/models.py:672
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:685 core/models.py:685
|
||||
#: build/lib/core/models.py:683 core/models.py:683
|
||||
msgid "Your reconciliation request has been processed.\n"
|
||||
" New documents are likely associated with your account:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:690 core/models.py:690
|
||||
#: build/lib/core/models.py:688 core/models.py:688
|
||||
msgid "Your accounts have been merged"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:695 core/models.py:695
|
||||
#: build/lib/core/models.py:693 core/models.py:693
|
||||
msgid "Click here to see"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:696 core/models.py:696
|
||||
#: build/lib/core/models.py:694 core/models.py:694
|
||||
msgid "See my documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:706 core/models.py:706
|
||||
#: build/lib/core/models.py:704 core/models.py:704
|
||||
msgid "CSV file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:711 core/models.py:711
|
||||
#: build/lib/core/models.py:709 core/models.py:709
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:721 core/models.py:721
|
||||
#: build/lib/core/models.py:719 core/models.py:719
|
||||
msgid "user reconciliation CSV import"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:722 core/models.py:722
|
||||
#: build/lib/core/models.py:720 core/models.py:720
|
||||
msgid "user reconciliation CSV imports"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:766 core/models.py:766
|
||||
#: build/lib/core/models.py:764 core/models.py:764
|
||||
#, python-brace-format
|
||||
msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" Reconciliation failed for the following email addresses:\n"
|
||||
@@ -347,175 +351,175 @@ msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" You can submit another request with the valid email addresses."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:774 core/models.py:774
|
||||
#: build/lib/core/models.py:772 core/models.py:772
|
||||
msgid "Reconciliation of your Docs accounts not completed"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:780 core/models.py:780
|
||||
#: build/lib/core/models.py:778 core/models.py:778
|
||||
msgid "Make a new request"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:879 core/models.py:879
|
||||
#: build/lib/core/models.py:877 core/models.py:877
|
||||
msgid "title"
|
||||
msgstr "naslov"
|
||||
|
||||
#: build/lib/core/models.py:880 core/models.py:880
|
||||
#: build/lib/core/models.py:878 core/models.py:878
|
||||
msgid "excerpt"
|
||||
msgstr "odlomek"
|
||||
|
||||
#: build/lib/core/models.py:929 core/models.py:929
|
||||
#: build/lib/core/models.py:927 core/models.py:927
|
||||
msgid "Document"
|
||||
msgstr "Dokument"
|
||||
|
||||
#: build/lib/core/models.py:930 core/models.py:930
|
||||
#: build/lib/core/models.py:928 core/models.py:928
|
||||
msgid "Documents"
|
||||
msgstr "Dokumenti"
|
||||
|
||||
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
|
||||
#: core/models.py:942 core/models.py:1346
|
||||
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
|
||||
#: core/models.py:940 core/models.py:1345
|
||||
msgid "Untitled Document"
|
||||
msgstr "Dokument brez naslova"
|
||||
|
||||
#: build/lib/core/models.py:1347 core/models.py:1347
|
||||
#: build/lib/core/models.py:1346 core/models.py:1346
|
||||
msgid "Open"
|
||||
msgstr "Odpri"
|
||||
|
||||
#: build/lib/core/models.py:1382 core/models.py:1382
|
||||
#: build/lib/core/models.py:1381 core/models.py:1381
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} je delil dokument z vami!"
|
||||
|
||||
#: build/lib/core/models.py:1386 core/models.py:1386
|
||||
#: build/lib/core/models.py:1385 core/models.py:1385
|
||||
#, 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:1392 core/models.py:1392
|
||||
#: build/lib/core/models.py:1391 core/models.py:1391
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} je delil dokument z vami: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
#: build/lib/core/models.py:1492 core/models.py:1492
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Dokument/sled povezave uporabnika"
|
||||
|
||||
#: build/lib/core/models.py:1494 core/models.py:1494
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Sledi povezav dokumenta/uporabnika"
|
||||
|
||||
#: build/lib/core/models.py:1500 core/models.py:1500
|
||||
#: build/lib/core/models.py:1499 core/models.py:1499
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
|
||||
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
#: build/lib/core/models.py:1522 core/models.py:1522
|
||||
msgid "Document favorite"
|
||||
msgstr "Priljubljeni dokument"
|
||||
|
||||
#: build/lib/core/models.py:1524 core/models.py:1524
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
msgid "Document favorites"
|
||||
msgstr "Priljubljeni dokumenti"
|
||||
|
||||
#: build/lib/core/models.py:1530 core/models.py:1530
|
||||
#: build/lib/core/models.py:1529 core/models.py:1529
|
||||
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:1552 core/models.py:1552
|
||||
#: build/lib/core/models.py:1551 core/models.py:1551
|
||||
msgid "Document/user relation"
|
||||
msgstr "Odnos dokument/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1553 core/models.py:1553
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
msgid "Document/user relations"
|
||||
msgstr "Odnosi dokument/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1559 core/models.py:1559
|
||||
#: build/lib/core/models.py:1558 core/models.py:1558
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Ta uporabnik je že v tem dokumentu."
|
||||
|
||||
#: build/lib/core/models.py:1565 core/models.py:1565
|
||||
#: build/lib/core/models.py:1564 core/models.py:1564
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ta ekipa je že v tem dokumentu."
|
||||
|
||||
#: build/lib/core/models.py:1571 core/models.py:1571
|
||||
#: build/lib/core/models.py:1570 core/models.py:1570
|
||||
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:1722 core/models.py:1722
|
||||
#: build/lib/core/models.py:1721 core/models.py:1721
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1723 core/models.py:1723
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1729 core/models.py:1729
|
||||
#: build/lib/core/models.py:1728 core/models.py:1728
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1786 core/models.py:1786
|
||||
#: build/lib/core/models.py:1785 core/models.py:1785
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1790 core/models.py:1790
|
||||
#: build/lib/core/models.py:1789 core/models.py:1789
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1796 core/models.py:1796
|
||||
#: build/lib/core/models.py:1795 core/models.py:1795
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
#: build/lib/core/models.py:1837 core/models.py:1837
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1839 core/models.py:1839
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
|
||||
#: core/models.py:1842 core/models.py:1894
|
||||
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
|
||||
#: core/models.py:1841 core/models.py:1893
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
#: build/lib/core/models.py:1888 core/models.py:1888
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1890 core/models.py:1890
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1939 core/models.py:1939
|
||||
#: build/lib/core/models.py:1938 core/models.py:1938
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
#: build/lib/core/models.py:1942 core/models.py:1942
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1944 core/models.py:1944
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1954 core/models.py:1954
|
||||
#: build/lib/core/models.py:1953 core/models.py:1953
|
||||
msgid "email address"
|
||||
msgstr "elektronski naslov"
|
||||
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
#: build/lib/core/models.py:1972 core/models.py:1972
|
||||
msgid "Document invitation"
|
||||
msgstr "Vabilo na dokument"
|
||||
|
||||
#: build/lib/core/models.py:1974 core/models.py:1974
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
msgid "Document invitations"
|
||||
msgstr "Vabila na dokument"
|
||||
|
||||
#: build/lib/core/models.py:1994 core/models.py:1994
|
||||
#: build/lib/core/models.py:1993 core/models.py:1993
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."
|
||||
|
||||
#: build/lib/impress/settings.py:702 impress/settings.py:702
|
||||
#: build/lib/impress/settings.py:808 impress/settings.py:808
|
||||
msgid "Docs AI"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
|
||||
"PO-Revision-Date: 2026-03-13 16:53\n"
|
||||
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 16:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Swedish\n"
|
||||
"Language: sv_SE\n"
|
||||
@@ -46,36 +46,40 @@ msgstr ""
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
|
||||
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Creator is me"
|
||||
msgstr "Skaparen är jag"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
|
||||
msgid "Favorite"
|
||||
msgstr "Favoriter"
|
||||
|
||||
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
|
||||
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Ett nytt dokument skapades åt dig!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
|
||||
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Du har beviljats äganderätt till ett nytt dokument:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
|
||||
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
|
||||
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
@@ -247,98 +251,98 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:378 core/models.py:378
|
||||
#: build/lib/core/models.py:376 core/models.py:376
|
||||
msgid "Active email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:379 core/models.py:379
|
||||
#: build/lib/core/models.py:377 core/models.py:377
|
||||
msgid "Email address to deactivate"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:406 core/models.py:406
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "Unique ID in the source file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
|
||||
#: core/models.py:708
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:413 core/models.py:413
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
|
||||
#: core/models.py:712
|
||||
msgid "Done"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
|
||||
#: core/models.py:713
|
||||
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
|
||||
#: core/models.py:711
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:423 core/models.py:423
|
||||
#: build/lib/core/models.py:421 core/models.py:421
|
||||
msgid "user reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 core/models.py:424
|
||||
#: build/lib/core/models.py:422 core/models.py:422
|
||||
msgid "user reconciliations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:662 core/models.py:662
|
||||
#: build/lib/core/models.py:660 core/models.py:660
|
||||
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
|
||||
" To confirm that you are the one who initiated the request\n"
|
||||
" and that this email belongs to you:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:668 core/models.py:668
|
||||
#: build/lib/core/models.py:666 core/models.py:666
|
||||
msgid "Confirm by clicking the link to start the reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
|
||||
#: core/models.py:779
|
||||
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
|
||||
#: core/models.py:777
|
||||
msgid "Click here"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:674 core/models.py:674
|
||||
#: build/lib/core/models.py:672 core/models.py:672
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:685 core/models.py:685
|
||||
#: build/lib/core/models.py:683 core/models.py:683
|
||||
msgid "Your reconciliation request has been processed.\n"
|
||||
" New documents are likely associated with your account:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:690 core/models.py:690
|
||||
#: build/lib/core/models.py:688 core/models.py:688
|
||||
msgid "Your accounts have been merged"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:695 core/models.py:695
|
||||
#: build/lib/core/models.py:693 core/models.py:693
|
||||
msgid "Click here to see"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:696 core/models.py:696
|
||||
#: build/lib/core/models.py:694 core/models.py:694
|
||||
msgid "See my documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:706 core/models.py:706
|
||||
#: build/lib/core/models.py:704 core/models.py:704
|
||||
msgid "CSV file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:711 core/models.py:711
|
||||
#: build/lib/core/models.py:709 core/models.py:709
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:721 core/models.py:721
|
||||
#: build/lib/core/models.py:719 core/models.py:719
|
||||
msgid "user reconciliation CSV import"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:722 core/models.py:722
|
||||
#: build/lib/core/models.py:720 core/models.py:720
|
||||
msgid "user reconciliation CSV imports"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:766 core/models.py:766
|
||||
#: build/lib/core/models.py:764 core/models.py:764
|
||||
#, python-brace-format
|
||||
msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" Reconciliation failed for the following email addresses:\n"
|
||||
@@ -347,175 +351,175 @@ msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" You can submit another request with the valid email addresses."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:774 core/models.py:774
|
||||
#: build/lib/core/models.py:772 core/models.py:772
|
||||
msgid "Reconciliation of your Docs accounts not completed"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:780 core/models.py:780
|
||||
#: build/lib/core/models.py:778 core/models.py:778
|
||||
msgid "Make a new request"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:879 core/models.py:879
|
||||
#: build/lib/core/models.py:877 core/models.py:877
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:880 core/models.py:880
|
||||
#: build/lib/core/models.py:878 core/models.py:878
|
||||
msgid "excerpt"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:929 core/models.py:929
|
||||
#: build/lib/core/models.py:927 core/models.py:927
|
||||
msgid "Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:930 core/models.py:930
|
||||
#: build/lib/core/models.py:928 core/models.py:928
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
|
||||
#: core/models.py:942 core/models.py:1346
|
||||
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
|
||||
#: core/models.py:940 core/models.py:1345
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1347 core/models.py:1347
|
||||
#: build/lib/core/models.py:1346 core/models.py:1346
|
||||
msgid "Open"
|
||||
msgstr "Öppna"
|
||||
|
||||
#: build/lib/core/models.py:1382 core/models.py:1382
|
||||
#: build/lib/core/models.py:1381 core/models.py:1381
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1386 core/models.py:1386
|
||||
#: build/lib/core/models.py:1385 core/models.py:1385
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1392 core/models.py:1392
|
||||
#: build/lib/core/models.py:1391 core/models.py:1391
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
#: build/lib/core/models.py:1492 core/models.py:1492
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1494 core/models.py:1494
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1500 core/models.py:1500
|
||||
#: build/lib/core/models.py:1499 core/models.py:1499
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
#: build/lib/core/models.py:1522 core/models.py:1522
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1524 core/models.py:1524
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1530 core/models.py:1530
|
||||
#: build/lib/core/models.py:1529 core/models.py:1529
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
#: build/lib/core/models.py:1551 core/models.py:1551
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1553 core/models.py:1553
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1559 core/models.py:1559
|
||||
#: build/lib/core/models.py:1558 core/models.py:1558
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1565 core/models.py:1565
|
||||
#: build/lib/core/models.py:1564 core/models.py:1564
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1571 core/models.py:1571
|
||||
#: build/lib/core/models.py:1570 core/models.py:1570
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
#: build/lib/core/models.py:1721 core/models.py:1721
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1723 core/models.py:1723
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1729 core/models.py:1729
|
||||
#: build/lib/core/models.py:1728 core/models.py:1728
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1786 core/models.py:1786
|
||||
#: build/lib/core/models.py:1785 core/models.py:1785
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1790 core/models.py:1790
|
||||
#: build/lib/core/models.py:1789 core/models.py:1789
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1796 core/models.py:1796
|
||||
#: build/lib/core/models.py:1795 core/models.py:1795
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
#: build/lib/core/models.py:1837 core/models.py:1837
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1839 core/models.py:1839
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
|
||||
#: core/models.py:1842 core/models.py:1894
|
||||
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
|
||||
#: core/models.py:1841 core/models.py:1893
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
#: build/lib/core/models.py:1888 core/models.py:1888
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1890 core/models.py:1890
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1939 core/models.py:1939
|
||||
#: build/lib/core/models.py:1938 core/models.py:1938
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
#: build/lib/core/models.py:1942 core/models.py:1942
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1944 core/models.py:1944
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1954 core/models.py:1954
|
||||
#: build/lib/core/models.py:1953 core/models.py:1953
|
||||
msgid "email address"
|
||||
msgstr "e-postadress"
|
||||
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
#: build/lib/core/models.py:1972 core/models.py:1972
|
||||
msgid "Document invitation"
|
||||
msgstr "Bjud in dokument"
|
||||
|
||||
#: build/lib/core/models.py:1974 core/models.py:1974
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
msgid "Document invitations"
|
||||
msgstr "Inbjudningar dokument"
|
||||
|
||||
#: build/lib/core/models.py:1994 core/models.py:1994
|
||||
#: build/lib/core/models.py:1993 core/models.py:1993
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Denna e-postadress är redan associerad med en registrerad användare."
|
||||
|
||||
#: build/lib/impress/settings.py:702 impress/settings.py:702
|
||||
#: build/lib/impress/settings.py:808 impress/settings.py:808
|
||||
msgid "Docs AI"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
|
||||
"PO-Revision-Date: 2026-03-13 16:53\n"
|
||||
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 16:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Turkish\n"
|
||||
"Language: tr_TR\n"
|
||||
@@ -46,36 +46,40 @@ msgstr ""
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
|
||||
msgid "Creator is me"
|
||||
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Masked"
|
||||
msgid "Creator is me"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
|
||||
msgid "Favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
|
||||
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
|
||||
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
|
||||
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
|
||||
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
@@ -247,98 +251,98 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:378 core/models.py:378
|
||||
#: build/lib/core/models.py:376 core/models.py:376
|
||||
msgid "Active email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:379 core/models.py:379
|
||||
#: build/lib/core/models.py:377 core/models.py:377
|
||||
msgid "Email address to deactivate"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:406 core/models.py:406
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "Unique ID in the source file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
|
||||
#: core/models.py:708
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:413 core/models.py:413
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
|
||||
#: core/models.py:712
|
||||
msgid "Done"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
|
||||
#: core/models.py:713
|
||||
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
|
||||
#: core/models.py:711
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:423 core/models.py:423
|
||||
#: build/lib/core/models.py:421 core/models.py:421
|
||||
msgid "user reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 core/models.py:424
|
||||
#: build/lib/core/models.py:422 core/models.py:422
|
||||
msgid "user reconciliations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:662 core/models.py:662
|
||||
#: build/lib/core/models.py:660 core/models.py:660
|
||||
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
|
||||
" To confirm that you are the one who initiated the request\n"
|
||||
" and that this email belongs to you:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:668 core/models.py:668
|
||||
#: build/lib/core/models.py:666 core/models.py:666
|
||||
msgid "Confirm by clicking the link to start the reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
|
||||
#: core/models.py:779
|
||||
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
|
||||
#: core/models.py:777
|
||||
msgid "Click here"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:674 core/models.py:674
|
||||
#: build/lib/core/models.py:672 core/models.py:672
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:685 core/models.py:685
|
||||
#: build/lib/core/models.py:683 core/models.py:683
|
||||
msgid "Your reconciliation request has been processed.\n"
|
||||
" New documents are likely associated with your account:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:690 core/models.py:690
|
||||
#: build/lib/core/models.py:688 core/models.py:688
|
||||
msgid "Your accounts have been merged"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:695 core/models.py:695
|
||||
#: build/lib/core/models.py:693 core/models.py:693
|
||||
msgid "Click here to see"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:696 core/models.py:696
|
||||
#: build/lib/core/models.py:694 core/models.py:694
|
||||
msgid "See my documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:706 core/models.py:706
|
||||
#: build/lib/core/models.py:704 core/models.py:704
|
||||
msgid "CSV file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:711 core/models.py:711
|
||||
#: build/lib/core/models.py:709 core/models.py:709
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:721 core/models.py:721
|
||||
#: build/lib/core/models.py:719 core/models.py:719
|
||||
msgid "user reconciliation CSV import"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:722 core/models.py:722
|
||||
#: build/lib/core/models.py:720 core/models.py:720
|
||||
msgid "user reconciliation CSV imports"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:766 core/models.py:766
|
||||
#: build/lib/core/models.py:764 core/models.py:764
|
||||
#, python-brace-format
|
||||
msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" Reconciliation failed for the following email addresses:\n"
|
||||
@@ -347,175 +351,175 @@ msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" You can submit another request with the valid email addresses."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:774 core/models.py:774
|
||||
#: build/lib/core/models.py:772 core/models.py:772
|
||||
msgid "Reconciliation of your Docs accounts not completed"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:780 core/models.py:780
|
||||
#: build/lib/core/models.py:778 core/models.py:778
|
||||
msgid "Make a new request"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:879 core/models.py:879
|
||||
#: build/lib/core/models.py:877 core/models.py:877
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:880 core/models.py:880
|
||||
#: build/lib/core/models.py:878 core/models.py:878
|
||||
msgid "excerpt"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:929 core/models.py:929
|
||||
#: build/lib/core/models.py:927 core/models.py:927
|
||||
msgid "Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:930 core/models.py:930
|
||||
#: build/lib/core/models.py:928 core/models.py:928
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
|
||||
#: core/models.py:942 core/models.py:1346
|
||||
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
|
||||
#: core/models.py:940 core/models.py:1345
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1347 core/models.py:1347
|
||||
#: build/lib/core/models.py:1346 core/models.py:1346
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1382 core/models.py:1382
|
||||
#: build/lib/core/models.py:1381 core/models.py:1381
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1386 core/models.py:1386
|
||||
#: build/lib/core/models.py:1385 core/models.py:1385
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1392 core/models.py:1392
|
||||
#: build/lib/core/models.py:1391 core/models.py:1391
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
#: build/lib/core/models.py:1492 core/models.py:1492
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1494 core/models.py:1494
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1500 core/models.py:1500
|
||||
#: build/lib/core/models.py:1499 core/models.py:1499
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
#: build/lib/core/models.py:1522 core/models.py:1522
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1524 core/models.py:1524
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1530 core/models.py:1530
|
||||
#: build/lib/core/models.py:1529 core/models.py:1529
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
#: build/lib/core/models.py:1551 core/models.py:1551
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1553 core/models.py:1553
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1559 core/models.py:1559
|
||||
#: build/lib/core/models.py:1558 core/models.py:1558
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1565 core/models.py:1565
|
||||
#: build/lib/core/models.py:1564 core/models.py:1564
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1571 core/models.py:1571
|
||||
#: build/lib/core/models.py:1570 core/models.py:1570
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
#: build/lib/core/models.py:1721 core/models.py:1721
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1723 core/models.py:1723
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1729 core/models.py:1729
|
||||
#: build/lib/core/models.py:1728 core/models.py:1728
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1786 core/models.py:1786
|
||||
#: build/lib/core/models.py:1785 core/models.py:1785
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1790 core/models.py:1790
|
||||
#: build/lib/core/models.py:1789 core/models.py:1789
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1796 core/models.py:1796
|
||||
#: build/lib/core/models.py:1795 core/models.py:1795
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
#: build/lib/core/models.py:1837 core/models.py:1837
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1839 core/models.py:1839
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
|
||||
#: core/models.py:1842 core/models.py:1894
|
||||
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
|
||||
#: core/models.py:1841 core/models.py:1893
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
#: build/lib/core/models.py:1888 core/models.py:1888
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1890 core/models.py:1890
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1939 core/models.py:1939
|
||||
#: build/lib/core/models.py:1938 core/models.py:1938
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
#: build/lib/core/models.py:1942 core/models.py:1942
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1944 core/models.py:1944
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1954 core/models.py:1954
|
||||
#: build/lib/core/models.py:1953 core/models.py:1953
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
#: build/lib/core/models.py:1972 core/models.py:1972
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1974 core/models.py:1974
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1994 core/models.py:1994
|
||||
#: build/lib/core/models.py:1993 core/models.py:1993
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/impress/settings.py:702 impress/settings.py:702
|
||||
#: build/lib/impress/settings.py:808 impress/settings.py:808
|
||||
msgid "Docs AI"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
|
||||
"PO-Revision-Date: 2026-03-13 16:53\n"
|
||||
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 16:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Ukrainian\n"
|
||||
"Language: uk_UA\n"
|
||||
@@ -46,36 +46,40 @@ msgstr "Ієрархічна структура"
|
||||
msgid "Title"
|
||||
msgstr "Заголовок"
|
||||
|
||||
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
|
||||
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
|
||||
msgid "Search"
|
||||
msgstr "Пошук"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Creator is me"
|
||||
msgstr "Творець — я"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
msgid "Masked"
|
||||
msgstr "Приховано"
|
||||
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
|
||||
msgid "Favorite"
|
||||
msgstr "Обране"
|
||||
|
||||
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
|
||||
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Новий документ був створений від вашого імені!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
|
||||
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Ви тепер є власником нового документа:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
|
||||
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
|
||||
msgid "This field is required."
|
||||
msgstr "Це поле є обов’язковим."
|
||||
|
||||
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr "Доступ до посилання '%(link_reach)s' заборонено на основі конфігурації батьківського документа."
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
|
||||
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "копія {title}"
|
||||
@@ -247,46 +251,46 @@ msgstr "користувач"
|
||||
msgid "users"
|
||||
msgstr "користувачі"
|
||||
|
||||
#: build/lib/core/models.py:378 core/models.py:378
|
||||
#: build/lib/core/models.py:376 core/models.py:376
|
||||
msgid "Active email address"
|
||||
msgstr "Активна електронна адреса"
|
||||
|
||||
#: build/lib/core/models.py:379 core/models.py:379
|
||||
#: build/lib/core/models.py:377 core/models.py:377
|
||||
msgid "Email address to deactivate"
|
||||
msgstr "Електронна адреса, що буде деактивована"
|
||||
|
||||
#: build/lib/core/models.py:406 core/models.py:406
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "Unique ID in the source file"
|
||||
msgstr "Унікальний ідентифікатор у вихідному файлі"
|
||||
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
|
||||
#: core/models.py:708
|
||||
msgid "Pending"
|
||||
msgstr "В очікуванні"
|
||||
|
||||
#: build/lib/core/models.py:413 core/models.py:413
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Ready"
|
||||
msgstr "Готово"
|
||||
|
||||
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
|
||||
#: core/models.py:712
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
msgid "Done"
|
||||
msgstr "Виконано"
|
||||
|
||||
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
|
||||
#: core/models.py:713
|
||||
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
|
||||
#: core/models.py:711
|
||||
msgid "Error"
|
||||
msgstr "Помилка"
|
||||
|
||||
#: build/lib/core/models.py:423 core/models.py:423
|
||||
#: build/lib/core/models.py:421 core/models.py:421
|
||||
msgid "user reconciliation"
|
||||
msgstr "узгодження користувачів"
|
||||
|
||||
#: build/lib/core/models.py:424 core/models.py:424
|
||||
#: build/lib/core/models.py:422 core/models.py:422
|
||||
msgid "user reconciliations"
|
||||
msgstr "узгодження користувачів"
|
||||
|
||||
#: build/lib/core/models.py:662 core/models.py:662
|
||||
#: build/lib/core/models.py:660 core/models.py:660
|
||||
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
|
||||
" To confirm that you are the one who initiated the request\n"
|
||||
" and that this email belongs to you:"
|
||||
@@ -294,54 +298,54 @@ msgstr "Ви запросили узгодження своїх облікови
|
||||
" Щоб підтвердити, що саме ви ініціювали запит\n"
|
||||
" і що ця електронна адреса належить вам:"
|
||||
|
||||
#: build/lib/core/models.py:668 core/models.py:668
|
||||
#: build/lib/core/models.py:666 core/models.py:666
|
||||
msgid "Confirm by clicking the link to start the reconciliation"
|
||||
msgstr "Підтвердіть, натиснувши на посилання, щоб почати узгодження"
|
||||
|
||||
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
|
||||
#: core/models.py:779
|
||||
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
|
||||
#: core/models.py:777
|
||||
msgid "Click here"
|
||||
msgstr "Натисніть тут"
|
||||
|
||||
#: build/lib/core/models.py:674 core/models.py:674
|
||||
#: build/lib/core/models.py:672 core/models.py:672
|
||||
msgid "Confirm"
|
||||
msgstr "Підтвердження"
|
||||
|
||||
#: build/lib/core/models.py:685 core/models.py:685
|
||||
#: build/lib/core/models.py:683 core/models.py:683
|
||||
msgid "Your reconciliation request has been processed.\n"
|
||||
" New documents are likely associated with your account:"
|
||||
msgstr "Ваш запит на узгодження оброблено.\n"
|
||||
" Нові документи, ймовірно, пов'язані з вашим обліковим записом:"
|
||||
|
||||
#: build/lib/core/models.py:690 core/models.py:690
|
||||
#: build/lib/core/models.py:688 core/models.py:688
|
||||
msgid "Your accounts have been merged"
|
||||
msgstr "Ваші облікові записи були об'єднані"
|
||||
|
||||
#: build/lib/core/models.py:695 core/models.py:695
|
||||
#: build/lib/core/models.py:693 core/models.py:693
|
||||
msgid "Click here to see"
|
||||
msgstr "Натисніть тут, щоб переглянути"
|
||||
|
||||
#: build/lib/core/models.py:696 core/models.py:696
|
||||
#: build/lib/core/models.py:694 core/models.py:694
|
||||
msgid "See my documents"
|
||||
msgstr "Переглянути мої документи"
|
||||
|
||||
#: build/lib/core/models.py:706 core/models.py:706
|
||||
#: build/lib/core/models.py:704 core/models.py:704
|
||||
msgid "CSV file"
|
||||
msgstr "CSV-файл"
|
||||
|
||||
#: build/lib/core/models.py:711 core/models.py:711
|
||||
#: build/lib/core/models.py:709 core/models.py:709
|
||||
msgid "Running"
|
||||
msgstr "Виконується"
|
||||
|
||||
#: build/lib/core/models.py:721 core/models.py:721
|
||||
#: build/lib/core/models.py:719 core/models.py:719
|
||||
msgid "user reconciliation CSV import"
|
||||
msgstr "імпорт CSV для узгодження користувачів"
|
||||
|
||||
#: build/lib/core/models.py:722 core/models.py:722
|
||||
#: build/lib/core/models.py:720 core/models.py:720
|
||||
msgid "user reconciliation CSV imports"
|
||||
msgstr "імпорт CSV для узгодження користувачів"
|
||||
|
||||
#: build/lib/core/models.py:766 core/models.py:766
|
||||
#: build/lib/core/models.py:764 core/models.py:764
|
||||
#, python-brace-format
|
||||
msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" Reconciliation failed for the following email addresses:\n"
|
||||
@@ -354,175 +358,175 @@ msgstr "Ваш запит на узгодження не був виконани
|
||||
" Перевірте, чи немає помилок.\n"
|
||||
" Ви можете надіслати інший запит із дійсними адресами електронної пошти."
|
||||
|
||||
#: build/lib/core/models.py:774 core/models.py:774
|
||||
#: build/lib/core/models.py:772 core/models.py:772
|
||||
msgid "Reconciliation of your Docs accounts not completed"
|
||||
msgstr "Узгодження ваших облікових записів не завершено"
|
||||
|
||||
#: build/lib/core/models.py:780 core/models.py:780
|
||||
#: build/lib/core/models.py:778 core/models.py:778
|
||||
msgid "Make a new request"
|
||||
msgstr "Зробити новий запит"
|
||||
|
||||
#: build/lib/core/models.py:879 core/models.py:879
|
||||
#: build/lib/core/models.py:877 core/models.py:877
|
||||
msgid "title"
|
||||
msgstr "заголовок"
|
||||
|
||||
#: build/lib/core/models.py:880 core/models.py:880
|
||||
#: build/lib/core/models.py:878 core/models.py:878
|
||||
msgid "excerpt"
|
||||
msgstr "уривок"
|
||||
|
||||
#: build/lib/core/models.py:929 core/models.py:929
|
||||
#: build/lib/core/models.py:927 core/models.py:927
|
||||
msgid "Document"
|
||||
msgstr "Документ"
|
||||
|
||||
#: build/lib/core/models.py:930 core/models.py:930
|
||||
#: build/lib/core/models.py:928 core/models.py:928
|
||||
msgid "Documents"
|
||||
msgstr "Документи"
|
||||
|
||||
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
|
||||
#: core/models.py:942 core/models.py:1346
|
||||
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
|
||||
#: core/models.py:940 core/models.py:1345
|
||||
msgid "Untitled Document"
|
||||
msgstr "Документ без назви"
|
||||
|
||||
#: build/lib/core/models.py:1347 core/models.py:1347
|
||||
#: build/lib/core/models.py:1346 core/models.py:1346
|
||||
msgid "Open"
|
||||
msgstr "Відкрити"
|
||||
|
||||
#: build/lib/core/models.py:1382 core/models.py:1382
|
||||
#: build/lib/core/models.py:1381 core/models.py:1381
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} ділиться з вами документом!"
|
||||
|
||||
#: build/lib/core/models.py:1386 core/models.py:1386
|
||||
#: build/lib/core/models.py:1385 core/models.py:1385
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
|
||||
|
||||
#: build/lib/core/models.py:1392 core/models.py:1392
|
||||
#: build/lib/core/models.py:1391 core/models.py:1391
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} ділиться з вами документом: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
#: build/lib/core/models.py:1492 core/models.py:1492
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Трасування посилання Документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1494 core/models.py:1494
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Трасування посилань Документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1500 core/models.py:1500
|
||||
#: build/lib/core/models.py:1499 core/models.py:1499
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
|
||||
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
#: build/lib/core/models.py:1522 core/models.py:1522
|
||||
msgid "Document favorite"
|
||||
msgstr "Обраний документ"
|
||||
|
||||
#: build/lib/core/models.py:1524 core/models.py:1524
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
msgid "Document favorites"
|
||||
msgstr "Обрані документи"
|
||||
|
||||
#: build/lib/core/models.py:1530 core/models.py:1530
|
||||
#: build/lib/core/models.py:1529 core/models.py:1529
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Цей документ вже вказаний як обраний для одного користувача."
|
||||
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
#: build/lib/core/models.py:1551 core/models.py:1551
|
||||
msgid "Document/user relation"
|
||||
msgstr "Відносини документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1553 core/models.py:1553
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
msgid "Document/user relations"
|
||||
msgstr "Відносини документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1559 core/models.py:1559
|
||||
#: build/lib/core/models.py:1558 core/models.py:1558
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Цей користувач вже має доступ до цього документу."
|
||||
|
||||
#: build/lib/core/models.py:1565 core/models.py:1565
|
||||
#: build/lib/core/models.py:1564 core/models.py:1564
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ця команда вже має доступ до цього документа."
|
||||
|
||||
#: build/lib/core/models.py:1571 core/models.py:1571
|
||||
#: build/lib/core/models.py:1570 core/models.py:1570
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Вкажіть користувача або команду, а не обох."
|
||||
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
#: build/lib/core/models.py:1721 core/models.py:1721
|
||||
msgid "Document ask for access"
|
||||
msgstr "Запит доступу до документа"
|
||||
|
||||
#: build/lib/core/models.py:1723 core/models.py:1723
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Запит доступу для документа"
|
||||
|
||||
#: build/lib/core/models.py:1729 core/models.py:1729
|
||||
#: build/lib/core/models.py:1728 core/models.py:1728
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "Цей користувач вже попросив доступ до цього документа."
|
||||
|
||||
#: build/lib/core/models.py:1786 core/models.py:1786
|
||||
#: build/lib/core/models.py:1785 core/models.py:1785
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} хоче отримати доступ до документа!"
|
||||
|
||||
#: build/lib/core/models.py:1790 core/models.py:1790
|
||||
#: build/lib/core/models.py:1789 core/models.py:1789
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} бажає отримати доступ до наступного документа:"
|
||||
|
||||
#: build/lib/core/models.py:1796 core/models.py:1796
|
||||
#: build/lib/core/models.py:1795 core/models.py:1795
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name} запитує доступ до документа: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
#: build/lib/core/models.py:1837 core/models.py:1837
|
||||
msgid "Thread"
|
||||
msgstr "Обговорення"
|
||||
|
||||
#: build/lib/core/models.py:1839 core/models.py:1839
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
msgid "Threads"
|
||||
msgstr "Обговорення"
|
||||
|
||||
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
|
||||
#: core/models.py:1842 core/models.py:1894
|
||||
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
|
||||
#: core/models.py:1841 core/models.py:1893
|
||||
msgid "Anonymous"
|
||||
msgstr "Анонім"
|
||||
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
#: build/lib/core/models.py:1888 core/models.py:1888
|
||||
msgid "Comment"
|
||||
msgstr "Коментар"
|
||||
|
||||
#: build/lib/core/models.py:1890 core/models.py:1890
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
msgid "Comments"
|
||||
msgstr "Коментарі"
|
||||
|
||||
#: build/lib/core/models.py:1939 core/models.py:1939
|
||||
#: build/lib/core/models.py:1938 core/models.py:1938
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr "Цим емодзі вже відреагували на цей коментар."
|
||||
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
#: build/lib/core/models.py:1942 core/models.py:1942
|
||||
msgid "Reaction"
|
||||
msgstr "Реакція"
|
||||
|
||||
#: build/lib/core/models.py:1944 core/models.py:1944
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
msgid "Reactions"
|
||||
msgstr "Реакції"
|
||||
|
||||
#: build/lib/core/models.py:1954 core/models.py:1954
|
||||
#: build/lib/core/models.py:1953 core/models.py:1953
|
||||
msgid "email address"
|
||||
msgstr "електронна адреса"
|
||||
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
#: build/lib/core/models.py:1972 core/models.py:1972
|
||||
msgid "Document invitation"
|
||||
msgstr "Запрошення до редагування документа"
|
||||
|
||||
#: build/lib/core/models.py:1974 core/models.py:1974
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
msgid "Document invitations"
|
||||
msgstr "Запрошення до редагування документів"
|
||||
|
||||
#: build/lib/core/models.py:1994 core/models.py:1994
|
||||
#: build/lib/core/models.py:1993 core/models.py:1993
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
|
||||
|
||||
#: build/lib/impress/settings.py:702 impress/settings.py:702
|
||||
#: build/lib/impress/settings.py:808 impress/settings.py:808
|
||||
msgid "Docs AI"
|
||||
msgstr "Docs ШІ"
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
|
||||
"PO-Revision-Date: 2026-03-13 16:53\n"
|
||||
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 16:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"Language: zh_CN\n"
|
||||
@@ -46,36 +46,40 @@ msgstr "樹狀結構"
|
||||
msgid "Title"
|
||||
msgstr "標題"
|
||||
|
||||
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
|
||||
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
msgid "Creator is me"
|
||||
msgstr "建立者是我"
|
||||
|
||||
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
msgid "Masked"
|
||||
msgstr "已隱藏"
|
||||
|
||||
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
|
||||
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
|
||||
msgid "Favorite"
|
||||
msgstr "我的最愛"
|
||||
|
||||
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
|
||||
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "已代表您建立新文件!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
|
||||
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "您已獲得新文件的所有權:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
|
||||
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
|
||||
msgid "This field is required."
|
||||
msgstr "此欄位為必填。"
|
||||
|
||||
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr "根據父文件設定,不允許連結範圍「%(link_reach)s」。"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
|
||||
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "{title} 的副本"
|
||||
@@ -247,98 +251,98 @@ msgstr "使用者"
|
||||
msgid "users"
|
||||
msgstr "使用者"
|
||||
|
||||
#: build/lib/core/models.py:378 core/models.py:378
|
||||
#: build/lib/core/models.py:376 core/models.py:376
|
||||
msgid "Active email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:379 core/models.py:379
|
||||
#: build/lib/core/models.py:377 core/models.py:377
|
||||
msgid "Email address to deactivate"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:406 core/models.py:406
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "Unique ID in the source file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
|
||||
#: core/models.py:708
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
|
||||
#: core/models.py:710
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:413 core/models.py:413
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
|
||||
#: core/models.py:712
|
||||
msgid "Done"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
|
||||
#: core/models.py:713
|
||||
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
|
||||
#: core/models.py:711
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:423 core/models.py:423
|
||||
#: build/lib/core/models.py:421 core/models.py:421
|
||||
msgid "user reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 core/models.py:424
|
||||
#: build/lib/core/models.py:422 core/models.py:422
|
||||
msgid "user reconciliations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:662 core/models.py:662
|
||||
#: build/lib/core/models.py:660 core/models.py:660
|
||||
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
|
||||
" To confirm that you are the one who initiated the request\n"
|
||||
" and that this email belongs to you:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:668 core/models.py:668
|
||||
#: build/lib/core/models.py:666 core/models.py:666
|
||||
msgid "Confirm by clicking the link to start the reconciliation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
|
||||
#: core/models.py:779
|
||||
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
|
||||
#: core/models.py:777
|
||||
msgid "Click here"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:674 core/models.py:674
|
||||
#: build/lib/core/models.py:672 core/models.py:672
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:685 core/models.py:685
|
||||
#: build/lib/core/models.py:683 core/models.py:683
|
||||
msgid "Your reconciliation request has been processed.\n"
|
||||
" New documents are likely associated with your account:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:690 core/models.py:690
|
||||
#: build/lib/core/models.py:688 core/models.py:688
|
||||
msgid "Your accounts have been merged"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:695 core/models.py:695
|
||||
#: build/lib/core/models.py:693 core/models.py:693
|
||||
msgid "Click here to see"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:696 core/models.py:696
|
||||
#: build/lib/core/models.py:694 core/models.py:694
|
||||
msgid "See my documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:706 core/models.py:706
|
||||
#: build/lib/core/models.py:704 core/models.py:704
|
||||
msgid "CSV file"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:711 core/models.py:711
|
||||
#: build/lib/core/models.py:709 core/models.py:709
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:721 core/models.py:721
|
||||
#: build/lib/core/models.py:719 core/models.py:719
|
||||
msgid "user reconciliation CSV import"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:722 core/models.py:722
|
||||
#: build/lib/core/models.py:720 core/models.py:720
|
||||
msgid "user reconciliation CSV imports"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:766 core/models.py:766
|
||||
#: build/lib/core/models.py:764 core/models.py:764
|
||||
#, python-brace-format
|
||||
msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" Reconciliation failed for the following email addresses:\n"
|
||||
@@ -347,175 +351,175 @@ msgid "Your request for reconciliation was unsuccessful.\n"
|
||||
" You can submit another request with the valid email addresses."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:774 core/models.py:774
|
||||
#: build/lib/core/models.py:772 core/models.py:772
|
||||
msgid "Reconciliation of your Docs accounts not completed"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:780 core/models.py:780
|
||||
#: build/lib/core/models.py:778 core/models.py:778
|
||||
msgid "Make a new request"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:879 core/models.py:879
|
||||
#: build/lib/core/models.py:877 core/models.py:877
|
||||
msgid "title"
|
||||
msgstr "標題"
|
||||
|
||||
#: build/lib/core/models.py:880 core/models.py:880
|
||||
#: build/lib/core/models.py:878 core/models.py:878
|
||||
msgid "excerpt"
|
||||
msgstr "摘要"
|
||||
|
||||
#: build/lib/core/models.py:929 core/models.py:929
|
||||
#: build/lib/core/models.py:927 core/models.py:927
|
||||
msgid "Document"
|
||||
msgstr "文件"
|
||||
|
||||
#: build/lib/core/models.py:930 core/models.py:930
|
||||
#: build/lib/core/models.py:928 core/models.py:928
|
||||
msgid "Documents"
|
||||
msgstr "文件"
|
||||
|
||||
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
|
||||
#: core/models.py:942 core/models.py:1346
|
||||
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
|
||||
#: core/models.py:940 core/models.py:1345
|
||||
msgid "Untitled Document"
|
||||
msgstr "未命名文件"
|
||||
|
||||
#: build/lib/core/models.py:1347 core/models.py:1347
|
||||
#: build/lib/core/models.py:1346 core/models.py:1346
|
||||
msgid "Open"
|
||||
msgstr "開啟"
|
||||
|
||||
#: build/lib/core/models.py:1382 core/models.py:1382
|
||||
#: build/lib/core/models.py:1381 core/models.py:1381
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} 與您分享了一份文件!"
|
||||
|
||||
#: build/lib/core/models.py:1386 core/models.py:1386
|
||||
#: build/lib/core/models.py:1385 core/models.py:1385
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} 邀請您以「{role}」角色參與以下文件:"
|
||||
|
||||
#: build/lib/core/models.py:1392 core/models.py:1392
|
||||
#: build/lib/core/models.py:1391 core/models.py:1391
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} 與您分享了一份文件:{title}"
|
||||
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
#: build/lib/core/models.py:1492 core/models.py:1492
|
||||
msgid "Document/user link trace"
|
||||
msgstr "文件/使用者連結追蹤"
|
||||
|
||||
#: build/lib/core/models.py:1494 core/models.py:1494
|
||||
#: build/lib/core/models.py:1493 core/models.py:1493
|
||||
msgid "Document/user link traces"
|
||||
msgstr "文件/使用者連結追蹤"
|
||||
|
||||
#: build/lib/core/models.py:1500 core/models.py:1500
|
||||
#: build/lib/core/models.py:1499 core/models.py:1499
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "此文件/使用者已存在連結追蹤。"
|
||||
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
#: build/lib/core/models.py:1522 core/models.py:1522
|
||||
msgid "Document favorite"
|
||||
msgstr "文件收藏"
|
||||
|
||||
#: build/lib/core/models.py:1524 core/models.py:1524
|
||||
#: build/lib/core/models.py:1523 core/models.py:1523
|
||||
msgid "Document favorites"
|
||||
msgstr "文件收藏"
|
||||
|
||||
#: build/lib/core/models.py:1530 core/models.py:1530
|
||||
#: build/lib/core/models.py:1529 core/models.py:1529
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "此使用者已將此文件加入收藏。"
|
||||
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
#: build/lib/core/models.py:1551 core/models.py:1551
|
||||
msgid "Document/user relation"
|
||||
msgstr "文件/使用者關聯"
|
||||
|
||||
#: build/lib/core/models.py:1553 core/models.py:1553
|
||||
#: build/lib/core/models.py:1552 core/models.py:1552
|
||||
msgid "Document/user relations"
|
||||
msgstr "文件/使用者關聯"
|
||||
|
||||
#: build/lib/core/models.py:1559 core/models.py:1559
|
||||
#: build/lib/core/models.py:1558 core/models.py:1558
|
||||
msgid "This user is already in this document."
|
||||
msgstr "此使用者已在此文件中。"
|
||||
|
||||
#: build/lib/core/models.py:1565 core/models.py:1565
|
||||
#: build/lib/core/models.py:1564 core/models.py:1564
|
||||
msgid "This team is already in this document."
|
||||
msgstr "此團隊已在此文件中。"
|
||||
|
||||
#: build/lib/core/models.py:1571 core/models.py:1571
|
||||
#: build/lib/core/models.py:1570 core/models.py:1570
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "必須設定使用者或團隊其中之一,不能同時設定兩者。"
|
||||
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
#: build/lib/core/models.py:1721 core/models.py:1721
|
||||
msgid "Document ask for access"
|
||||
msgstr "要求文件存取權"
|
||||
|
||||
#: build/lib/core/models.py:1723 core/models.py:1723
|
||||
#: build/lib/core/models.py:1722 core/models.py:1722
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "要求文件存取權"
|
||||
|
||||
#: build/lib/core/models.py:1729 core/models.py:1729
|
||||
#: build/lib/core/models.py:1728 core/models.py:1728
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "此使用者已要求過存取此文件的權限。"
|
||||
|
||||
#: build/lib/core/models.py:1786 core/models.py:1786
|
||||
#: build/lib/core/models.py:1785 core/models.py:1785
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} 想要存取文件!"
|
||||
|
||||
#: build/lib/core/models.py:1790 core/models.py:1790
|
||||
#: build/lib/core/models.py:1789 core/models.py:1789
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} 想要存取以下文件:"
|
||||
|
||||
#: build/lib/core/models.py:1796 core/models.py:1796
|
||||
#: build/lib/core/models.py:1795 core/models.py:1795
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name} 正要求存取文件:{title}"
|
||||
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
#: build/lib/core/models.py:1837 core/models.py:1837
|
||||
msgid "Thread"
|
||||
msgstr "對話串"
|
||||
|
||||
#: build/lib/core/models.py:1839 core/models.py:1839
|
||||
#: build/lib/core/models.py:1838 core/models.py:1838
|
||||
msgid "Threads"
|
||||
msgstr "對話串"
|
||||
|
||||
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
|
||||
#: core/models.py:1842 core/models.py:1894
|
||||
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
|
||||
#: core/models.py:1841 core/models.py:1893
|
||||
msgid "Anonymous"
|
||||
msgstr "匿名"
|
||||
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
#: build/lib/core/models.py:1888 core/models.py:1888
|
||||
msgid "Comment"
|
||||
msgstr "評論"
|
||||
|
||||
#: build/lib/core/models.py:1890 core/models.py:1890
|
||||
#: build/lib/core/models.py:1889 core/models.py:1889
|
||||
msgid "Comments"
|
||||
msgstr "評論"
|
||||
|
||||
#: build/lib/core/models.py:1939 core/models.py:1939
|
||||
#: build/lib/core/models.py:1938 core/models.py:1938
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr "此評論已標記過此表情符號。"
|
||||
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
#: build/lib/core/models.py:1942 core/models.py:1942
|
||||
msgid "Reaction"
|
||||
msgstr "回應"
|
||||
|
||||
#: build/lib/core/models.py:1944 core/models.py:1944
|
||||
#: build/lib/core/models.py:1943 core/models.py:1943
|
||||
msgid "Reactions"
|
||||
msgstr "回應"
|
||||
|
||||
#: build/lib/core/models.py:1954 core/models.py:1954
|
||||
#: build/lib/core/models.py:1953 core/models.py:1953
|
||||
msgid "email address"
|
||||
msgstr "電子郵件地址"
|
||||
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
#: build/lib/core/models.py:1972 core/models.py:1972
|
||||
msgid "Document invitation"
|
||||
msgstr "文件邀請"
|
||||
|
||||
#: build/lib/core/models.py:1974 core/models.py:1974
|
||||
#: build/lib/core/models.py:1973 core/models.py:1973
|
||||
msgid "Document invitations"
|
||||
msgstr "文件邀請"
|
||||
|
||||
#: build/lib/core/models.py:1994 core/models.py:1994
|
||||
#: build/lib/core/models.py:1993 core/models.py:1993
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "此電子郵件地址已與已註冊使用者關聯。"
|
||||
|
||||
#: build/lib/impress/settings.py:702 impress/settings.py:702
|
||||
#: build/lib/impress/settings.py:808 impress/settings.py:808
|
||||
msgid "Docs AI"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "4.8.3"
|
||||
version = "4.8.4"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -59,10 +59,10 @@ dependencies = [
|
||||
"pycrdt==0.12.47",
|
||||
"pydantic==2.12.5",
|
||||
"pydantic-ai-slim[openai,logfire,web]==1.58.0",
|
||||
"PyJWT==2.11.0",
|
||||
"PyJWT==2.12.0",
|
||||
"python-magic==0.4.27",
|
||||
"redis<6.0.0",
|
||||
"requests==2.32.5",
|
||||
"requests==2.33.0",
|
||||
"sentry-sdk==2.53.0",
|
||||
"uvicorn==0.41.0",
|
||||
"whitenoise==6.12.0",
|
||||
|
||||
@@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
createDoc,
|
||||
getMenuItem,
|
||||
mockedDocument,
|
||||
overrideConfig,
|
||||
verifyDocName,
|
||||
@@ -47,9 +46,9 @@ test.describe('Doc AI feature', () => {
|
||||
|
||||
await page.locator('.bn-block-outer').last().fill('Anything');
|
||||
await page.getByText('Anything').selectText();
|
||||
expect(
|
||||
await page.locator('button[data-test="convertMarkdown"]').count(),
|
||||
).toBe(1);
|
||||
await expect(
|
||||
page.locator('button[data-test="convertMarkdown"]'),
|
||||
).toHaveCount(1);
|
||||
await expect(
|
||||
page.getByRole('button', { name: config.selector, exact: true }),
|
||||
).toBeHidden();
|
||||
@@ -179,18 +178,32 @@ test.describe('Doc AI feature', () => {
|
||||
|
||||
await page.getByRole('button', { name: 'AI', exact: true }).click();
|
||||
|
||||
await expect(getMenuItem(page, 'Use as prompt')).toBeVisible();
|
||||
await expect(getMenuItem(page, 'Rephrase')).toBeVisible();
|
||||
await expect(getMenuItem(page, 'Summarize')).toBeVisible();
|
||||
await expect(getMenuItem(page, 'Correct')).toBeVisible();
|
||||
await expect(getMenuItem(page, 'Language')).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Use as prompt' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Rephrase' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Summarize' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: 'Correct' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Language' }),
|
||||
).toBeVisible();
|
||||
|
||||
await getMenuItem(page, 'Language').hover();
|
||||
await expect(getMenuItem(page, 'English', { exact: true })).toBeVisible();
|
||||
await expect(getMenuItem(page, 'French', { exact: true })).toBeVisible();
|
||||
await expect(getMenuItem(page, 'German', { exact: true })).toBeVisible();
|
||||
await page.getByRole('menuitem', { name: 'Language' }).hover();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'English', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'French', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'German', exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await getMenuItem(page, 'German', { exact: true }).click();
|
||||
await page.getByRole('menuitem', { name: 'German', exact: true }).click();
|
||||
|
||||
await expect(editor.getByText('Hallo Welt')).toBeVisible();
|
||||
});
|
||||
@@ -256,15 +269,23 @@ test.describe('Doc AI feature', () => {
|
||||
await page.getByRole('button', { name: 'AI', exact: true }).click();
|
||||
|
||||
if (ai_transform) {
|
||||
await expect(getMenuItem(page, 'Use as prompt')).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Use as prompt' }),
|
||||
).toBeVisible();
|
||||
} else {
|
||||
await expect(getMenuItem(page, 'Use as prompt')).toBeHidden();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Use as prompt' }),
|
||||
).toBeHidden();
|
||||
}
|
||||
|
||||
if (ai_translate) {
|
||||
await expect(getMenuItem(page, 'Language')).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Language' }),
|
||||
).toBeVisible();
|
||||
} else {
|
||||
await expect(getMenuItem(page, 'Language')).toBeHidden();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Language' }),
|
||||
).toBeHidden();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test';
|
||||
import {
|
||||
closeHeaderMenu,
|
||||
createDoc,
|
||||
getMenuItem,
|
||||
getOtherBrowserName,
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
@@ -152,7 +151,7 @@ test.describe('Doc Comments', () => {
|
||||
// Edit Comment
|
||||
await thread.getByText('This is a comment').first().hover();
|
||||
await thread.locator('[data-test="moreactions"]').first().click();
|
||||
await getMenuItem(thread, 'Edit comment').click();
|
||||
await thread.getByRole('menuitem', { name: 'Edit comment' }).click();
|
||||
const commentEditor = thread.getByText('This is a comment').first();
|
||||
await commentEditor.fill('This is an edited comment');
|
||||
const saveBtn = thread.locator('button[data-test="save"]').first();
|
||||
@@ -177,7 +176,7 @@ test.describe('Doc Comments', () => {
|
||||
// Delete second comment
|
||||
await thread.getByText('This is a second comment').first().hover();
|
||||
await thread.locator('[data-test="moreactions"]').first().click();
|
||||
await getMenuItem(thread, 'Delete comment').click();
|
||||
await thread.getByRole('menuitem', { name: 'Delete comment' }).click();
|
||||
await expect(
|
||||
thread.getByText('This is a second comment').first(),
|
||||
).toBeHidden();
|
||||
@@ -210,7 +209,7 @@ test.describe('Doc Comments', () => {
|
||||
|
||||
await thread.getByText('This is a new comment').first().hover();
|
||||
await thread.locator('[data-test="moreactions"]').first().click();
|
||||
await getMenuItem(thread, 'Delete comment').click();
|
||||
await thread.getByRole('menuitem', { name: 'Delete comment' }).click();
|
||||
|
||||
await expect(editor.getByText('Hello')).not.toHaveClass('bn-thread-mark');
|
||||
await expect(editor.getByText('Hello')).toHaveCSS(
|
||||
|
||||
@@ -5,7 +5,6 @@ import cs from 'convert-stream';
|
||||
|
||||
import {
|
||||
createDoc,
|
||||
getMenuItem,
|
||||
goToGridDoc,
|
||||
overrideConfig,
|
||||
verifyDocName,
|
||||
@@ -148,20 +147,18 @@ test.describe('Doc Editor', () => {
|
||||
const wsClosePromise = webSocket.waitForEvent('close');
|
||||
|
||||
await selectVisibility.click();
|
||||
await getMenuItem(page, 'Connected').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Connected' }).click();
|
||||
|
||||
// Assert that the doc reconnects to the ws
|
||||
const wsClose = await wsClosePromise;
|
||||
expect(wsClose.isClosed()).toBeTruthy();
|
||||
|
||||
// Check the ws is connected again
|
||||
webSocketPromise = page.waitForEvent('websocket', (webSocket) => {
|
||||
webSocket = await page.waitForEvent('websocket', (webSocket) => {
|
||||
return webSocket
|
||||
.url()
|
||||
.includes('ws://localhost:4444/collaboration/ws/?room=');
|
||||
});
|
||||
|
||||
webSocket = await webSocketPromise;
|
||||
framesentPromise = webSocket.waitForEvent('framesent');
|
||||
framesent = await framesentPromise;
|
||||
expect(framesent.payload).not.toBeNull();
|
||||
@@ -578,12 +575,10 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
await page.reload();
|
||||
|
||||
responseCanEditPromise = page.waitForResponse(
|
||||
responseCanEdit = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/can-edit/`) && response.status() === 200,
|
||||
);
|
||||
|
||||
responseCanEdit = await responseCanEditPromise;
|
||||
expect(responseCanEdit.ok()).toBeTruthy();
|
||||
|
||||
jsonCanEdit = (await responseCanEdit.json()) as { can_edit: boolean };
|
||||
@@ -609,7 +604,7 @@ test.describe('Doc Editor', () => {
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
await page.getByTestId('doc-access-mode').click();
|
||||
await getMenuItem(page, 'Reading').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Reading' }).click();
|
||||
|
||||
// Close the modal
|
||||
await page.getByRole('button', { name: 'close' }).first().click();
|
||||
|
||||
@@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test';
|
||||
import {
|
||||
createDoc,
|
||||
getGridRow,
|
||||
getMenuItem,
|
||||
getOtherBrowserName,
|
||||
mockedListDocs,
|
||||
toggleHeaderMenu,
|
||||
@@ -207,7 +206,7 @@ test.describe('Doc grid move', () => {
|
||||
const row = await getGridRow(page, titleDoc1);
|
||||
await row.getByText(`more_horiz`).click();
|
||||
|
||||
await getMenuItem(page, 'Move into a doc').click();
|
||||
await page.getByRole('menuitem', { name: 'Move into a doc' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('dialog').getByRole('heading', { name: 'Move' }),
|
||||
@@ -295,7 +294,7 @@ test.describe('Doc grid move', () => {
|
||||
const row = await getGridRow(page, titleDoc1);
|
||||
await row.getByText(`more_horiz`).click();
|
||||
|
||||
await getMenuItem(page, 'Move into a doc').click();
|
||||
await page.getByRole('menuitem', { name: 'Move into a doc' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('dialog').getByRole('heading', { name: 'Move' }),
|
||||
@@ -342,7 +341,9 @@ test.describe('Doc grid move', () => {
|
||||
`doc-share-access-request-row-${emailRequest}`,
|
||||
);
|
||||
await container.getByTestId('doc-role-dropdown').click();
|
||||
await getMenuItem(otherPage, 'Administrator').click();
|
||||
await otherPage
|
||||
.getByRole('menuitemradio', { name: 'Administrator' })
|
||||
.click();
|
||||
await container.getByRole('button', { name: 'Approve' }).click();
|
||||
|
||||
await expect(otherPage.getByText('Access Requests')).toBeHidden();
|
||||
@@ -353,7 +354,7 @@ test.describe('Doc grid move', () => {
|
||||
await page.reload();
|
||||
await row.getByText(`more_horiz`).click();
|
||||
|
||||
await getMenuItem(page, 'Move into a doc').click();
|
||||
await page.getByRole('menuitem', { name: 'Move into a doc' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('dialog').getByRole('heading', { name: 'Move' }),
|
||||
@@ -399,7 +400,7 @@ test.describe('Doc grid dnd mobile', () => {
|
||||
await expect(page.getByTestId('docs-grid')).toBeVisible();
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
|
||||
await expect(docsGrid.getByRole('row').first()).toBeVisible();
|
||||
await expect(docsGrid.getByRole('listitem').first()).toBeVisible();
|
||||
await expect(docsGrid.locator('.--docs--grid-droppable')).toHaveCount(0);
|
||||
|
||||
await createDoc(page, 'Draggable doc mobile', browserName, 1, true);
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
createDoc,
|
||||
getGridRow,
|
||||
getMenuItem,
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
import { createDoc, getGridRow, verifyDocName } from './utils-common';
|
||||
import { addNewMember, connectOtherUserToDoc } from './utils-share';
|
||||
|
||||
type SmallDoc = {
|
||||
@@ -81,7 +76,7 @@ test.describe('Documents Grid mobile', () => {
|
||||
await expect(docsGrid).toBeVisible();
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
|
||||
const rows = docsGrid.getByRole('row');
|
||||
const rows = docsGrid.getByRole('listitem');
|
||||
const row = rows.filter({
|
||||
hasText: 'My mocked document',
|
||||
});
|
||||
@@ -104,7 +99,7 @@ test.describe('Document grid item options', () => {
|
||||
const row = await getGridRow(page, docTitle);
|
||||
await row.getByText(`more_horiz`).click();
|
||||
|
||||
await getMenuItem(page, 'Share').click();
|
||||
await page.getByRole('menuitem', { name: 'Share' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('dialog').getByText('Share the document'),
|
||||
@@ -120,7 +115,7 @@ test.describe('Document grid item options', () => {
|
||||
|
||||
// Pin
|
||||
await row.getByText(`more_horiz`).click();
|
||||
await getMenuItem(page, 'Pin').click();
|
||||
await page.getByRole('menuitem', { name: 'Pin' }).click();
|
||||
|
||||
// Check is pinned
|
||||
await expect(row.getByTestId('doc-pinned-icon')).toBeVisible();
|
||||
@@ -147,7 +142,7 @@ test.describe('Document grid item options', () => {
|
||||
const row = await getGridRow(page, docTitle);
|
||||
await row.getByText(`more_horiz`).click();
|
||||
|
||||
await getMenuItem(page, 'Delete').click();
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Delete a doc' }),
|
||||
@@ -294,6 +289,29 @@ test.describe('Documents Grid', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('opens a document with keyboard (Tab + Enter)', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await page.goto('/');
|
||||
|
||||
const [docTitle] = await createDoc(page, 'keyboard-nav-test', browserName);
|
||||
|
||||
await page.goto('/');
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
|
||||
const row = await getGridRow(page, docTitle);
|
||||
const link = row.getByRole('link').first();
|
||||
|
||||
await link.focus();
|
||||
await expect(link).toBeFocused();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await expect(page).toHaveURL(/\/docs\//);
|
||||
await verifyDocName(page, docTitle);
|
||||
});
|
||||
|
||||
test('checks the infinite scroll', async ({ page }) => {
|
||||
let docs: SmallDoc[];
|
||||
const responsePromisePage1 = page.waitForResponse((response) => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test';
|
||||
import {
|
||||
createDoc,
|
||||
getGridRow,
|
||||
getMenuItem,
|
||||
goToGridDoc,
|
||||
mockedDocument,
|
||||
verifyDocName,
|
||||
@@ -79,7 +78,7 @@ test.describe('Doc Header', () => {
|
||||
|
||||
await page.getByTestId('doc-visibility').click();
|
||||
|
||||
await getMenuItem(page, 'Public').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Public' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'close' }).first().click();
|
||||
|
||||
@@ -153,8 +152,10 @@ test.describe('Doc Header', () => {
|
||||
|
||||
const emojiPicker = page.locator('.--docs--doc-title').getByRole('button');
|
||||
const optionMenu = page.getByLabel('Open the document options');
|
||||
const addEmojiMenuItem = getMenuItem(page, 'Add emoji');
|
||||
const removeEmojiMenuItem = getMenuItem(page, 'Remove emoji');
|
||||
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();
|
||||
@@ -208,7 +209,7 @@ test.describe('Doc Header', () => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await getMenuItem(page, 'Delete document').click();
|
||||
await page.getByRole('menuitem', { name: 'Delete document' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Delete a doc' }),
|
||||
@@ -236,7 +237,7 @@ test.describe('Doc Header', () => {
|
||||
hasText: randomDoc,
|
||||
});
|
||||
|
||||
expect(await row.count()).toBe(0);
|
||||
await expect(row).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('it checks the options available if administrator', async ({ page }) => {
|
||||
@@ -270,10 +271,12 @@ test.describe('Doc Header', () => {
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await expect(getMenuItem(page, 'Delete document')).toBeDisabled();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Delete document' }),
|
||||
).toBeDisabled();
|
||||
|
||||
// Click somewhere else to close the options
|
||||
await page.click('body', { position: { x: 0, y: 0 } });
|
||||
await page.locator('body').click({ position: { x: 0, y: 0 } });
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
@@ -293,7 +296,7 @@ test.describe('Doc Header', () => {
|
||||
|
||||
await invitationRole.click();
|
||||
|
||||
await getMenuItem(page, 'Remove access').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Remove access' }).click();
|
||||
await expect(invitationCard).toBeHidden();
|
||||
|
||||
const memberCard = shareModal.getByLabel('List members card');
|
||||
@@ -305,7 +308,9 @@ test.describe('Doc Header', () => {
|
||||
await expect(roles).toBeVisible();
|
||||
|
||||
await roles.click();
|
||||
await expect(getMenuItem(page, 'Remove access')).toBeEnabled();
|
||||
await expect(
|
||||
page.getByRole('menuitemradio', { name: 'Remove access' }),
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
test('it checks the options available if editor', async ({ page }) => {
|
||||
@@ -345,10 +350,12 @@ test.describe('Doc Header', () => {
|
||||
).toBeVisible();
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await expect(getMenuItem(page, 'Delete document')).toBeDisabled();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Delete document' }),
|
||||
).toBeDisabled();
|
||||
|
||||
// Click somewhere else to close the options
|
||||
await page.click('body', { position: { x: 0, y: 0 } });
|
||||
await page.locator('body').click({ position: { x: 0, y: 0 } });
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
@@ -415,10 +422,12 @@ test.describe('Doc Header', () => {
|
||||
).toBeVisible();
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await expect(getMenuItem(page, 'Delete document')).toBeDisabled();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Delete document' }),
|
||||
).toBeDisabled();
|
||||
|
||||
// Click somewhere else to close the options
|
||||
await page.click('body', { position: { x: 0, y: 0 } });
|
||||
await page.locator('body').click({ position: { x: 0, y: 0 } });
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
@@ -473,7 +482,7 @@ test.describe('Doc Header', () => {
|
||||
|
||||
// Copy content to clipboard
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await getMenuItem(page, 'Copy as Markdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Copy as Markdown' }).click();
|
||||
await expect(
|
||||
page.getByText('Copied as Markdown to clipboard'),
|
||||
).toBeVisible();
|
||||
@@ -537,7 +546,7 @@ test.describe('Doc Header', () => {
|
||||
.click();
|
||||
|
||||
// Pin
|
||||
await getMenuItem(page, 'Pin').click();
|
||||
await page.getByRole('menuitem', { name: 'Pin' }).click();
|
||||
await page
|
||||
.getByRole('button', { name: 'Open the document options' })
|
||||
.click();
|
||||
@@ -558,11 +567,11 @@ test.describe('Doc Header', () => {
|
||||
.click();
|
||||
|
||||
// Unpin
|
||||
await getMenuItem(page, 'Unpin').click();
|
||||
await page.getByRole('menuitem', { name: 'Unpin' }).click();
|
||||
await page
|
||||
.getByRole('button', { name: 'Open the document options' })
|
||||
.click();
|
||||
await expect(getMenuItem(page, 'Pin')).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: 'Pin' })).toBeVisible();
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
@@ -580,7 +589,7 @@ test.describe('Doc Header', () => {
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await getMenuItem(page, 'Duplicate').click();
|
||||
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
||||
await expect(
|
||||
page.getByText('Document duplicated successfully!'),
|
||||
).toBeVisible();
|
||||
@@ -595,7 +604,7 @@ test.describe('Doc Header', () => {
|
||||
await expect(row.getByText(duplicateTitle)).toBeVisible();
|
||||
|
||||
await row.getByText(`more_horiz`).click();
|
||||
await getMenuItem(page, 'Duplicate').click();
|
||||
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
||||
const duplicateDuplicateTitle = 'Copy of ' + duplicateTitle;
|
||||
await page.getByText(duplicateDuplicateTitle).click();
|
||||
await expect(page.getByText('Hello Duplicated World')).toBeVisible();
|
||||
@@ -628,7 +637,7 @@ test.describe('Doc Header', () => {
|
||||
|
||||
const currentUrl = page.url();
|
||||
|
||||
await getMenuItem(page, 'Duplicate').click();
|
||||
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
||||
|
||||
await expect(page).not.toHaveURL(new RegExp(currentUrl));
|
||||
|
||||
@@ -667,8 +676,10 @@ test.describe('Documents Header mobile', () => {
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Copy link' })).toBeHidden();
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await expect(getMenuItem(page, 'Copy link')).toBeVisible();
|
||||
await getMenuItem(page, 'Share').click();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Copy link' }),
|
||||
).toBeVisible();
|
||||
await page.getByRole('menuitem', { name: 'Share' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Copy link' })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -691,7 +702,7 @@ test.describe('Documents Header mobile', () => {
|
||||
await goToGridDoc(page);
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await getMenuItem(page, 'Share').click();
|
||||
await page.getByRole('menuitem', { name: 'Share' }).click();
|
||||
|
||||
const shareModal = page.getByRole('dialog', {
|
||||
name: 'Share the document',
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from 'path';
|
||||
|
||||
import { Page, expect, test } from '@playwright/test';
|
||||
|
||||
import { overrideConfig } from './utils-common';
|
||||
import { getEditor } from './utils-editor';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -10,6 +11,16 @@ test.beforeEach(async ({ page }) => {
|
||||
});
|
||||
|
||||
test.describe('Doc Import', () => {
|
||||
test('import is not enabled if flag is disabled', async ({ page }) => {
|
||||
await overrideConfig(page, {
|
||||
CONVERSION_UPLOAD_ENABLED: false,
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page.getByLabel('Open the upload dialog')).toBeHidden();
|
||||
});
|
||||
|
||||
test('it imports 2 docs with the import icon', async ({ page }) => {
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.getByLabel('Open the upload dialog').click();
|
||||
@@ -177,5 +188,5 @@ const dragAndDropFiles = async (
|
||||
return dt;
|
||||
}, filesData);
|
||||
|
||||
await page.dispatchEvent(selector, 'drop', { dataTransfer });
|
||||
await page.locator(selector).dispatchEvent('drop', { dataTransfer });
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, getMenuItem, verifyDocName } from './utils-common';
|
||||
import { createDoc, verifyDocName } from './utils-common';
|
||||
import { updateShareLink } from './utils-share';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
@@ -53,17 +53,19 @@ test.describe('Inherited share accesses', () => {
|
||||
await expect(docVisibilityCard.getByText('Reading')).toBeVisible();
|
||||
|
||||
await docVisibilityCard.getByText('Reading').click();
|
||||
await getMenuItem(page, 'Editing').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Editing' }).click();
|
||||
|
||||
await expect(docVisibilityCard.getByText('Reading')).toBeHidden();
|
||||
await expect(docVisibilityCard.getByText('Editing')).toBeVisible();
|
||||
|
||||
// Verify inherited link
|
||||
await docVisibilityCard.getByText('Connected').click();
|
||||
await expect(getMenuItem(page, 'Private')).toBeDisabled();
|
||||
await expect(
|
||||
page.getByRole('menuitemradio', { name: 'Private' }),
|
||||
).toBeDisabled();
|
||||
|
||||
// Update child link
|
||||
await getMenuItem(page, 'Public').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Public' }).click();
|
||||
|
||||
await expect(docVisibilityCard.getByText('Connected')).toBeHidden();
|
||||
await expect(
|
||||
|
||||
@@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test';
|
||||
import {
|
||||
BROWSERS,
|
||||
createDoc,
|
||||
getMenuItem,
|
||||
keyCloakSignIn,
|
||||
randomName,
|
||||
verifyDocName,
|
||||
@@ -17,6 +16,41 @@ test.describe('Document create member', () => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('it checks search hints', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'select-multi-users', browserName, 1);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
const shareModal = page.getByLabel('Share the document');
|
||||
await expect(shareModal.getByText('Document owner')).toBeVisible();
|
||||
|
||||
const inputSearch = page.getByTestId('quick-search-input');
|
||||
await inputSearch.fill('u');
|
||||
await expect(shareModal.getByText('Document owner')).toBeHidden();
|
||||
await expect(
|
||||
shareModal.getByText('Type at least 3 characters to display user names'),
|
||||
).toBeVisible();
|
||||
await inputSearch.fill('user');
|
||||
await expect(
|
||||
shareModal.getByText('Type at least 3 characters to display user names'),
|
||||
).toBeHidden();
|
||||
await expect(shareModal.getByText('Choose a user')).toBeVisible();
|
||||
await inputSearch.fill('anything');
|
||||
await expect(shareModal.getByText('Choose a user')).toBeHidden();
|
||||
await expect(
|
||||
shareModal.getByText(
|
||||
'No results. Type a full email address to invite someone.',
|
||||
),
|
||||
).toBeVisible();
|
||||
await inputSearch.fill('anything@test.com');
|
||||
await expect(
|
||||
shareModal.getByText(
|
||||
'No results. Type a full email address to invite someone.',
|
||||
),
|
||||
).toBeHidden();
|
||||
await expect(shareModal.getByText('Choose the email')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it selects 2 users and 1 invitation', async ({ page, browserName }) => {
|
||||
const inputFill = 'user.test';
|
||||
const responsePromise = page.waitForResponse(
|
||||
@@ -76,13 +110,21 @@ test.describe('Document create member', () => {
|
||||
|
||||
// Check roles are displayed
|
||||
await list.getByTestId('doc-role-dropdown').click();
|
||||
await expect(getMenuItem(page, 'Reader')).toBeVisible();
|
||||
await expect(getMenuItem(page, 'Editor')).toBeVisible();
|
||||
await expect(getMenuItem(page, 'Owner')).toBeVisible();
|
||||
await expect(getMenuItem(page, 'Administrator')).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('menuitemradio', { name: 'Reader' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('menuitemradio', { name: 'Editor' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('menuitemradio', { name: 'Owner' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('menuitemradio', { name: 'Administrator' }),
|
||||
).toBeVisible();
|
||||
|
||||
// Validate
|
||||
await getMenuItem(page, 'Administrator').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Administrator' }).click();
|
||||
await page.getByTestId('doc-share-invite-button').click();
|
||||
|
||||
// Check invitation added
|
||||
@@ -128,7 +170,7 @@ test.describe('Document create member', () => {
|
||||
// Choose a role
|
||||
const container = page.getByTestId('doc-share-add-member-list');
|
||||
await container.getByTestId('doc-role-dropdown').click();
|
||||
await getMenuItem(page, 'Owner').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Owner' }).click();
|
||||
|
||||
const responsePromiseCreateInvitation = page.waitForResponse(
|
||||
(response) =>
|
||||
@@ -146,7 +188,7 @@ test.describe('Document create member', () => {
|
||||
|
||||
// Choose a role
|
||||
await container.getByTestId('doc-role-dropdown').click();
|
||||
await getMenuItem(page, 'Owner').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Owner' }).click();
|
||||
|
||||
const responsePromiseCreateInvitationFail = page.waitForResponse(
|
||||
(response) =>
|
||||
@@ -183,7 +225,7 @@ test.describe('Document create member', () => {
|
||||
// Choose a role
|
||||
const container = page.getByTestId('doc-share-add-member-list');
|
||||
await container.getByTestId('doc-role-dropdown').click();
|
||||
await getMenuItem(page, 'Administrator').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Administrator' }).click();
|
||||
|
||||
const responsePromiseCreateInvitation = page.waitForResponse(
|
||||
(response) =>
|
||||
@@ -210,13 +252,13 @@ test.describe('Document create member', () => {
|
||||
);
|
||||
|
||||
await userInvitation.getByTestId('doc-role-dropdown').click();
|
||||
await getMenuItem(page, 'Reader').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Reader' }).click();
|
||||
|
||||
const responsePatchInvitation = await responsePromisePatchInvitation;
|
||||
expect(responsePatchInvitation.ok()).toBeTruthy();
|
||||
|
||||
await userInvitation.getByTestId('doc-role-dropdown').click();
|
||||
await getMenuItem(page, 'Remove access').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Remove access' }).click();
|
||||
|
||||
await expect(userInvitation).toBeHidden();
|
||||
});
|
||||
@@ -268,7 +310,7 @@ test.describe('Document create member', () => {
|
||||
`doc-share-access-request-row-${emailRequest}`,
|
||||
);
|
||||
await container.getByTestId('doc-role-dropdown').click();
|
||||
await getMenuItem(page, 'Administrator').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Administrator' }).click();
|
||||
await container.getByRole('button', { name: 'Approve' }).click();
|
||||
|
||||
await expect(page.getByText('Access Requests')).toBeHidden();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, getMenuItem, verifyDocName } from './utils-common';
|
||||
import { createDoc, verifyDocName } from './utils-common';
|
||||
import { addNewMember } from './utils-share';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -160,7 +160,9 @@ test.describe('Document list members', () => {
|
||||
`You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.`,
|
||||
);
|
||||
await expect(soloOwner).toBeVisible();
|
||||
await expect(getMenuItem(page, 'Administrator')).toBeDisabled();
|
||||
await expect(
|
||||
page.getByRole('menuitemradio', { name: 'Administrator' }),
|
||||
).toBeDisabled();
|
||||
|
||||
await list.click({
|
||||
force: true, // Force click to close the dropdown
|
||||
@@ -183,18 +185,20 @@ test.describe('Document list members', () => {
|
||||
});
|
||||
|
||||
await currentUserRole.click();
|
||||
await getMenuItem(page, 'Administrator').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Administrator' }).click();
|
||||
await list.click();
|
||||
await expect(currentUserRole).toBeVisible();
|
||||
|
||||
await newUserRoles.click();
|
||||
await expect(getMenuItem(page, 'Owner')).toBeDisabled();
|
||||
await expect(
|
||||
page.getByRole('menuitemradio', { name: 'Owner' }),
|
||||
).toBeDisabled();
|
||||
await list.click({
|
||||
force: true, // Force click to close the dropdown
|
||||
});
|
||||
|
||||
await currentUserRole.click();
|
||||
await getMenuItem(page, 'Reader').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Reader' }).click();
|
||||
await list.click({
|
||||
force: true, // Force click to close the dropdown
|
||||
});
|
||||
@@ -234,11 +238,11 @@ test.describe('Document list members', () => {
|
||||
await expect(userReader).toBeVisible();
|
||||
|
||||
await userReaderRole.click();
|
||||
await getMenuItem(page, 'Remove access').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Remove access' }).click();
|
||||
await expect(userReader).toBeHidden();
|
||||
|
||||
await mySelfRole.click();
|
||||
await getMenuItem(page, 'Remove access').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Remove access' }).click();
|
||||
await expect(
|
||||
page.getByText('Insufficient access rights to view the document.'),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, getMenuItem, verifyDocName } from './utils-common';
|
||||
import { createDoc, verifyDocName } from './utils-common';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -136,9 +136,13 @@ test.describe('Document search', () => {
|
||||
|
||||
await filters.click();
|
||||
await filters.getByRole('button', { name: 'Current doc' }).click();
|
||||
await expect(getMenuItem(page, 'All docs')).toBeVisible();
|
||||
await expect(getMenuItem(page, 'Current doc')).toBeVisible();
|
||||
await getMenuItem(page, 'All docs').click();
|
||||
await expect(
|
||||
page.getByRole('menuitemcheckbox', { name: 'All docs' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('menuitemcheckbox', { name: 'Current doc' }),
|
||||
).toBeVisible();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'All docs' }).click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -3,13 +3,13 @@ import { expect, test } from '@playwright/test';
|
||||
import {
|
||||
createDoc,
|
||||
expectLoginPage,
|
||||
getMenuItem,
|
||||
keyCloakSignIn,
|
||||
updateDocTitle,
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
import { addNewMember } from './utils-share';
|
||||
import {
|
||||
addChild,
|
||||
clickOnAddRootSubPage,
|
||||
createRootSubPage,
|
||||
getTreeRow,
|
||||
@@ -20,6 +20,137 @@ test.describe('Doc Tree', () => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('check the tree pagination', async ({ page, browserName }) => {
|
||||
await page.route(/.*\/documents\/.*\/children\//, async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
const pageId = url.searchParams.get('page') ?? '1';
|
||||
|
||||
const response = {
|
||||
count: 40,
|
||||
next: `http://localhost:8071/api/v1.0/documents/anything/children/?page=${parseInt(pageId) + 1}`,
|
||||
previous:
|
||||
parseInt(pageId) > 1
|
||||
? `http://localhost:8071/api/v1.0/documents/anything/children/?page=${parseInt(pageId) - 1}`
|
||||
: null,
|
||||
results: Array.from({ length: 20 }, (_, i) => ({
|
||||
id: `doc-child-${pageId}-${i}`,
|
||||
abilities: {
|
||||
accesses_manage: true,
|
||||
accesses_view: true,
|
||||
ai_proxy: true,
|
||||
ai_transform: true,
|
||||
ai_translate: true,
|
||||
attachment_upload: true,
|
||||
media_check: true,
|
||||
can_edit: true,
|
||||
children_list: true,
|
||||
children_create: true,
|
||||
collaboration_auth: true,
|
||||
comment: true,
|
||||
content: true,
|
||||
cors_proxy: true,
|
||||
descendants: true,
|
||||
destroy: true,
|
||||
duplicate: true,
|
||||
favorite: true,
|
||||
link_configuration: true,
|
||||
invite_owner: true,
|
||||
mask: true,
|
||||
move: true,
|
||||
partial_update: true,
|
||||
restore: true,
|
||||
retrieve: true,
|
||||
media_auth: true,
|
||||
link_select_options: {
|
||||
restricted: null,
|
||||
authenticated: ['reader', 'commenter', 'editor'],
|
||||
public: ['reader', 'commenter', 'editor'],
|
||||
},
|
||||
tree: true,
|
||||
update: true,
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
search: true,
|
||||
},
|
||||
ancestors_link_reach: 'restricted',
|
||||
ancestors_link_role: null,
|
||||
computed_link_reach: 'restricted',
|
||||
computed_link_role: null,
|
||||
created_at: '2026-03-27T14:44:12.398544Z',
|
||||
creator: '40d339e9-cd97-4fdc-b65f-0a809c7e2db9',
|
||||
deleted_at: null,
|
||||
depth: 3,
|
||||
excerpt: null,
|
||||
is_favorite: false,
|
||||
link_role: 'reader',
|
||||
link_reach: 'restricted',
|
||||
nb_accesses_ancestors: 1,
|
||||
nb_accesses_direct: 0,
|
||||
numchild: 0,
|
||||
path: `000000p00000010000001-${pageId}-${i}`,
|
||||
title: `doc-child-${pageId}-${i}`,
|
||||
updated_at: '2026-03-27T14:44:26.691903Z',
|
||||
user_role: 'owner',
|
||||
})),
|
||||
};
|
||||
|
||||
if (request.method().includes('GET')) {
|
||||
await route.fulfill({
|
||||
json: response,
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
const [title] = await createDoc(
|
||||
page,
|
||||
'doc-tree-pagination',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
const pageParentUrl = page.url();
|
||||
|
||||
const titleChild = await addChild({
|
||||
page,
|
||||
browserName,
|
||||
docParent: title,
|
||||
docName: 'doc-tree-pagination-child',
|
||||
});
|
||||
|
||||
await addChild({
|
||||
page,
|
||||
browserName,
|
||||
docParent: titleChild,
|
||||
docName: 'doc-tree-pagination-child-2',
|
||||
});
|
||||
|
||||
await page.goto(pageParentUrl);
|
||||
|
||||
await verifyDocName(page, title);
|
||||
|
||||
const docTree = page.getByTestId('doc-tree');
|
||||
await expect(docTree).toBeVisible();
|
||||
await docTree.getByText('keyboard_arrow_right').click();
|
||||
await docTree
|
||||
.getByRole('button', {
|
||||
name: `Open document ${titleChild}`,
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(docTree.getByText('doc-child-1-19')).toBeVisible();
|
||||
await expect(docTree.locator('.c__spinner')).toBeVisible();
|
||||
await docTree.getByText('doc-child-1-19').hover();
|
||||
await expect(
|
||||
docTree.getByText('doc-child-2-1', {
|
||||
exact: true,
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('check the reorder of sub pages', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'doc-tree-content', browserName, 1);
|
||||
const addButton = page.getByTestId('new-doc-button');
|
||||
@@ -43,15 +174,12 @@ test.describe('Doc Tree', () => {
|
||||
await expect(secondSubPageItem).toBeVisible();
|
||||
|
||||
// Check the position of the sub pages
|
||||
const allSubPageItems = await docTree
|
||||
.getByTestId(/^doc-sub-page-item/)
|
||||
.all();
|
||||
|
||||
expect(allSubPageItems.length).toBe(2);
|
||||
const allSubPageItems = docTree.getByTestId(/^doc-sub-page-item/);
|
||||
await expect(allSubPageItems).toHaveCount(2);
|
||||
|
||||
// Check that elements are in the correct order
|
||||
await expect(allSubPageItems[0].getByText('first move')).toBeVisible();
|
||||
await expect(allSubPageItems[1].getByText('second move')).toBeVisible();
|
||||
await expect(allSubPageItems.nth(0).getByText('first move')).toBeVisible();
|
||||
await expect(allSubPageItems.nth(1).getByText('second move')).toBeVisible();
|
||||
|
||||
// Will move the first sub page to the second position
|
||||
const firstSubPageBoundingBox = await firstSubPageItem.boundingBox();
|
||||
@@ -91,17 +219,15 @@ test.describe('Doc Tree', () => {
|
||||
await expect(secondSubPageItem).toBeVisible();
|
||||
|
||||
// Check that elements are in the correct order
|
||||
const allSubPageItemsAfterReload = await docTree
|
||||
.getByTestId(/^doc-sub-page-item/)
|
||||
.all();
|
||||
|
||||
expect(allSubPageItemsAfterReload.length).toBe(2);
|
||||
const allSubPageItemsAfterReload =
|
||||
docTree.getByTestId(/^doc-sub-page-item/);
|
||||
await expect(allSubPageItemsAfterReload).toHaveCount(2);
|
||||
|
||||
await expect(
|
||||
allSubPageItemsAfterReload[0].getByText('second move'),
|
||||
allSubPageItemsAfterReload.nth(0).getByText('second move'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
allSubPageItemsAfterReload[1].getByText('first move'),
|
||||
allSubPageItemsAfterReload.nth(1).getByText('first move'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -163,7 +289,7 @@ test.describe('Doc Tree', () => {
|
||||
);
|
||||
const currentUserRole = currentUser.getByTestId('doc-role-dropdown');
|
||||
await currentUserRole.click();
|
||||
await getMenuItem(page, 'Administrator').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Administrator' }).click();
|
||||
await list.click();
|
||||
|
||||
await page.getByRole('button', { name: 'Ok' }).click();
|
||||
@@ -193,10 +319,9 @@ test.describe('Doc Tree', () => {
|
||||
const menu = child.getByText(`more_horiz`);
|
||||
await menu.click();
|
||||
|
||||
await expect(getMenuItem(page, 'Move to my docs')).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true',
|
||||
);
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Move to my docs' }),
|
||||
).toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
|
||||
test('keyboard navigation with Enter key opens documents', async ({
|
||||
@@ -340,7 +465,9 @@ test.describe('Doc Tree', () => {
|
||||
await row.hover();
|
||||
const menu = row.getByText(`more_horiz`);
|
||||
await menu.click();
|
||||
await expect(getMenuItem(page, 'Remove emoji')).toBeHidden();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Remove emoji' }),
|
||||
).toBeHidden();
|
||||
|
||||
// Close the menu
|
||||
await page.keyboard.press('Escape');
|
||||
@@ -360,7 +487,7 @@ test.describe('Doc Tree', () => {
|
||||
// Now remove the emoji using the new action
|
||||
await row.hover();
|
||||
await menu.click();
|
||||
await getMenuItem(page, 'Remove emoji').click();
|
||||
await page.getByRole('menuitem', { name: 'Remove emoji' }).click();
|
||||
|
||||
await expect(row.getByText('😀')).toBeHidden();
|
||||
await expect(titleEmojiPicker).toBeHidden();
|
||||
@@ -390,7 +517,7 @@ test.describe('Doc Tree: Inheritance', () => {
|
||||
const selectVisibility = page.getByTestId('doc-visibility');
|
||||
await selectVisibility.click();
|
||||
|
||||
await getMenuItem(page, 'Public').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Public' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document visibility has been updated.'),
|
||||
|
||||
@@ -2,7 +2,6 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
createDoc,
|
||||
getMenuItem,
|
||||
goToGridDoc,
|
||||
mockedDocument,
|
||||
verifyDocName,
|
||||
@@ -21,7 +20,7 @@ test.describe('Doc Version', () => {
|
||||
|
||||
// Initially, there is no version
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await getMenuItem(page, 'Version history').click();
|
||||
await page.getByRole('menuitem', { name: 'Version history' }).click();
|
||||
await expect(page.getByText('History', { exact: true })).toBeVisible();
|
||||
|
||||
const modal = page.getByRole('dialog', { name: 'Version history' });
|
||||
@@ -75,14 +74,14 @@ test.describe('Doc Version', () => {
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await getMenuItem(page, 'Version history').click();
|
||||
await page.getByRole('menuitem', { name: 'Version history' }).click();
|
||||
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(page.getByText('History', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('status')).toBeHidden();
|
||||
const items = await panel.locator('.version-item').all();
|
||||
expect(items.length).toBe(2);
|
||||
await items[1].click();
|
||||
const items = panel.locator('.version-item');
|
||||
await expect(items).toHaveCount(2);
|
||||
await items.nth(1).click();
|
||||
|
||||
await expect(modal.getByText('Hello World')).toBeVisible();
|
||||
await expect(modal.getByText('It will create a version')).toBeHidden();
|
||||
@@ -90,7 +89,7 @@ test.describe('Doc Version', () => {
|
||||
modal.locator('div[data-content-type="callout"]').first(),
|
||||
).toBeHidden();
|
||||
|
||||
await items[0].click();
|
||||
await items.nth(0).click();
|
||||
|
||||
await expect(modal.getByText('Hello World')).toBeVisible();
|
||||
await expect(modal.getByText('It will create a version')).toBeVisible();
|
||||
@@ -101,7 +100,7 @@ test.describe('Doc Version', () => {
|
||||
modal.getByText('It will create a second version'),
|
||||
).toBeHidden();
|
||||
|
||||
await items[1].click();
|
||||
await items.nth(1).click();
|
||||
|
||||
await expect(modal.getByText('Hello World')).toBeVisible();
|
||||
await expect(modal.getByText('It will create a version')).toBeHidden();
|
||||
@@ -125,7 +124,9 @@ test.describe('Doc Version', () => {
|
||||
await verifyDocName(page, 'Mocked document');
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await expect(getMenuItem(page, 'Version history')).toBeDisabled();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Version history' }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
test('it restores the doc version', async ({ page, browserName }) => {
|
||||
@@ -152,7 +153,7 @@ test.describe('Doc Version', () => {
|
||||
await expect(page.getByText('World')).toBeVisible();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await getMenuItem(page, 'Version history').click();
|
||||
await page.getByRole('menuitem', { name: 'Version history' }).click();
|
||||
|
||||
const modal = page.getByRole('dialog', { name: 'Version history' });
|
||||
const panel = modal.getByLabel('Version list');
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
BROWSERS,
|
||||
createDoc,
|
||||
expectLoginPage,
|
||||
getMenuItem,
|
||||
keyCloakSignIn,
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
@@ -47,17 +46,21 @@ test.describe('Doc Visibility', () => {
|
||||
|
||||
await expect(selectVisibility.getByText('Private')).toBeVisible();
|
||||
|
||||
await expect(getMenuItem(page, 'Read only')).toBeHidden();
|
||||
await expect(getMenuItem(page, 'Can read and edit')).toBeHidden();
|
||||
await expect(
|
||||
page.getByRole('menuitemradio', { name: 'Read only' }),
|
||||
).toBeHidden();
|
||||
await expect(
|
||||
page.getByRole('menuitemradio', { name: 'Can read and edit' }),
|
||||
).toBeHidden();
|
||||
|
||||
await selectVisibility.click();
|
||||
await getMenuItem(page, 'Connected').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Connected' }).click();
|
||||
|
||||
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
|
||||
|
||||
await selectVisibility.click();
|
||||
|
||||
await getMenuItem(page, 'Public').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Public' }).click();
|
||||
|
||||
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
|
||||
});
|
||||
@@ -202,7 +205,7 @@ test.describe('Doc Visibility: Public', () => {
|
||||
const selectVisibility = page.getByTestId('doc-visibility');
|
||||
await selectVisibility.click();
|
||||
|
||||
await getMenuItem(page, 'Public').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Public' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document visibility has been updated.'),
|
||||
@@ -210,7 +213,7 @@ test.describe('Doc Visibility: Public', () => {
|
||||
|
||||
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
|
||||
await page.getByTestId('doc-access-mode').click();
|
||||
await getMenuItem(page, 'Reading').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Reading' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document visibility has been updated.').first(),
|
||||
@@ -296,14 +299,14 @@ test.describe('Doc Visibility: Public', () => {
|
||||
const selectVisibility = page.getByTestId('doc-visibility');
|
||||
await selectVisibility.click();
|
||||
|
||||
await getMenuItem(page, 'Public').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Public' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document visibility has been updated.'),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByTestId('doc-access-mode').click();
|
||||
await getMenuItem(page, 'Editing').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Editing' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document visibility has been updated.').first(),
|
||||
@@ -387,7 +390,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
const selectVisibility = page.getByTestId('doc-visibility');
|
||||
await selectVisibility.click();
|
||||
await getMenuItem(page, 'Connected').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Connected' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document visibility has been updated.'),
|
||||
@@ -435,7 +438,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
const selectVisibility = page.getByTestId('doc-visibility');
|
||||
await selectVisibility.click();
|
||||
await getMenuItem(page, 'Connected').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Connected' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document visibility has been updated.'),
|
||||
@@ -533,7 +536,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
const selectVisibility = page.getByTestId('doc-visibility');
|
||||
await selectVisibility.click();
|
||||
await getMenuItem(page, 'Connected').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Connected' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document visibility has been updated.'),
|
||||
@@ -541,7 +544,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
|
||||
const urlDoc = page.url();
|
||||
await page.getByTestId('doc-access-mode').click();
|
||||
await getMenuItem(page, 'Editing').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Editing' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document visibility has been updated.').first(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { getMenuItem, overrideConfig } from './utils-common';
|
||||
import { overrideConfig } from './utils-common';
|
||||
|
||||
test.describe('Footer', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
@@ -47,7 +47,7 @@ test.describe('Footer', () => {
|
||||
// Check the translation
|
||||
const header = page.locator('header').first();
|
||||
await header.getByRole('button').getByText('English').click();
|
||||
await getMenuItem(page, 'Français').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Français' }).click();
|
||||
|
||||
await expect(
|
||||
page.locator('footer').getByText('Mentions légales'),
|
||||
@@ -131,7 +131,7 @@ test.describe('Footer', () => {
|
||||
// Check the translation
|
||||
const header = page.locator('header').first();
|
||||
await header.getByRole('button').getByText('English').click();
|
||||
await getMenuItem(page, 'Français').click();
|
||||
await page.getByRole('menuitemradio', { name: 'Français' }).click();
|
||||
|
||||
await expect(
|
||||
page
|
||||
|
||||
@@ -2,7 +2,6 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
TestLanguage,
|
||||
getMenuItem,
|
||||
overrideConfig,
|
||||
waitForLanguageSwitch,
|
||||
} from './utils-common';
|
||||
@@ -45,7 +44,7 @@ test.describe('Help feature', () => {
|
||||
|
||||
await page.getByRole('button', { name: 'Open help menu' }).click();
|
||||
|
||||
await getMenuItem(page, 'Onboarding').click();
|
||||
await page.getByRole('menuitem', { name: 'Onboarding' }).click();
|
||||
|
||||
const modal = page.getByTestId('onboarding-modal');
|
||||
await expect(modal).toBeVisible();
|
||||
@@ -88,7 +87,7 @@ test.describe('Help feature', () => {
|
||||
|
||||
test('closes modal with Skip button', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Open help menu' }).click();
|
||||
await getMenuItem(page, 'Onboarding').click();
|
||||
await page.getByRole('menuitem', { name: 'Onboarding' }).click();
|
||||
|
||||
const modal = page.getByTestId('onboarding-modal');
|
||||
await expect(modal).toBeVisible();
|
||||
@@ -109,7 +108,7 @@ test.describe('Help feature', () => {
|
||||
|
||||
await page.getByRole('button', { name: "Ouvrir le menu d'aide" }).click();
|
||||
|
||||
await getMenuItem(page, 'Premiers pas').click();
|
||||
await page.getByRole('menuitem', { name: 'Premiers pas' }).click();
|
||||
|
||||
const modal = page.getByLabel('Apprenez les principes fondamentaux');
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ test.describe('Language', () => {
|
||||
|
||||
await expect(page.locator('[role="menu"]')).toBeVisible();
|
||||
|
||||
const menuItems = page.locator('[role="menuitem"], [role="menuitemradio"]');
|
||||
const menuItems = page.locator('[role="menuitemradio"]');
|
||||
await expect(menuItems.first()).toBeVisible();
|
||||
|
||||
await menuItems.first().click();
|
||||
|
||||
@@ -3,16 +3,6 @@ import path from 'path';
|
||||
|
||||
import { Locator, Page, TestInfo, expect } from '@playwright/test';
|
||||
|
||||
/** Returns a locator for a menu item (handles both menuitem and menuitemradio roles) */
|
||||
export const getMenuItem = (
|
||||
context: Page | Locator,
|
||||
name: string,
|
||||
options?: { exact?: boolean },
|
||||
): Locator =>
|
||||
context
|
||||
.getByRole('menuitem', { name, exact: options?.exact })
|
||||
.or(context.getByRole('menuitemradio', { name, exact: options?.exact }));
|
||||
|
||||
import theme_customization from '../../../../../backend/impress/configuration/theme/default.json';
|
||||
|
||||
export type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||
@@ -30,6 +20,7 @@ export const CONFIG = {
|
||||
CRISP_WEBSITE_ID: null,
|
||||
COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/',
|
||||
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY: true,
|
||||
CONVERSION_UPLOAD_ENABLED: true,
|
||||
CONVERSION_FILE_EXTENSIONS_ALLOWED: ['.docx', '.md'],
|
||||
CONVERSION_FILE_MAX_SIZE: 20971520,
|
||||
ENVIRONMENT: 'development',
|
||||
@@ -193,11 +184,11 @@ export const verifyDocName = async (page: Page, docName: string) => {
|
||||
};
|
||||
|
||||
export const getGridRow = async (page: Page, title: string) => {
|
||||
const docsGrid = page.getByRole('grid');
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid).toBeVisible();
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
|
||||
const rows = docsGrid.getByRole('row');
|
||||
const rows = docsGrid.getByRole('listitem');
|
||||
|
||||
const row = rows
|
||||
.filter({
|
||||
@@ -225,7 +216,7 @@ export const goToGridDoc = async (
|
||||
await expect(docsGrid).toBeVisible();
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
|
||||
const rows = docsGrid.getByRole('row');
|
||||
const rows = docsGrid.getByRole('listitem');
|
||||
|
||||
const row = title
|
||||
? rows.filter({
|
||||
@@ -392,12 +383,12 @@ export async function waitForLanguageSwitch(
|
||||
|
||||
await languagePicker.click();
|
||||
|
||||
await getMenuItem(page, lang.label).click();
|
||||
await page.getByRole('menuitemradio', { name: lang.label }).click();
|
||||
}
|
||||
|
||||
export const clickInEditorMenu = async (page: Page, textButton: string) => {
|
||||
await page.getByRole('button', { name: 'Open the document options' }).click();
|
||||
await getMenuItem(page, textButton).click();
|
||||
await page.getByRole('menuitem', { name: textButton }).click();
|
||||
};
|
||||
|
||||
export const clickInGridMenu = async (
|
||||
@@ -408,7 +399,7 @@ export const clickInGridMenu = async (
|
||||
await row
|
||||
.getByRole('button', { name: /Open the menu of actions for the document/ })
|
||||
.click();
|
||||
await getMenuItem(page, textButton).click();
|
||||
await page.getByRole('menuitem', { name: textButton }).click();
|
||||
};
|
||||
|
||||
export const writeReport = async (
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Page, chromium, expect } from '@playwright/test';
|
||||
|
||||
import {
|
||||
BrowserName,
|
||||
getMenuItem,
|
||||
getOtherBrowserName,
|
||||
keyCloakSignIn,
|
||||
verifyDocName,
|
||||
@@ -40,7 +39,7 @@ export const addNewMember = async (
|
||||
|
||||
// Choose a role
|
||||
await page.getByTestId('doc-role-dropdown').click();
|
||||
await getMenuItem(page, role).click();
|
||||
await page.getByRole('menuitemradio', { name: role }).click();
|
||||
await page.getByTestId('doc-share-invite-button').click();
|
||||
|
||||
return users[index].email;
|
||||
@@ -52,7 +51,7 @@ export const updateShareLink = async (
|
||||
linkRole?: LinkRole | null,
|
||||
) => {
|
||||
await page.getByTestId('doc-visibility').click();
|
||||
await getMenuItem(page, linkReach).click();
|
||||
await page.getByRole('menuitemradio', { name: linkReach }).click();
|
||||
|
||||
const visibilityUpdatedText = page
|
||||
.getByText('The document visibility has been updated')
|
||||
@@ -62,7 +61,7 @@ export const updateShareLink = async (
|
||||
|
||||
if (linkRole) {
|
||||
await page.getByTestId('doc-access-mode').click();
|
||||
await getMenuItem(page, linkRole).click();
|
||||
await page.getByRole('menuitemradio', { name: linkRole }).click();
|
||||
await expect(visibilityUpdatedText).toBeVisible();
|
||||
}
|
||||
};
|
||||
@@ -77,7 +76,7 @@ export const updateRoleUser = async (
|
||||
const currentUser = list.getByTestId(`doc-share-member-row-${email}`);
|
||||
const currentUserRole = currentUser.getByTestId('doc-role-dropdown');
|
||||
await currentUserRole.click();
|
||||
await getMenuItem(page, role).click();
|
||||
await page.getByRole('menuitemradio', { name: role }).click();
|
||||
await list.click();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "4.8.3",
|
||||
"version": "4.8.4",
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"author": "DINUM",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "4.8.3",
|
||||
"version": "4.8.4",
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"author": "DINUM",
|
||||
"license": "MIT",
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ag-media/react-pdf-table": "2.0.3",
|
||||
"@ai-sdk/openai": "3.0.19",
|
||||
"@ai-sdk/openai": "3.0.45",
|
||||
"@blocknote/code-block": "0.47.1",
|
||||
"@blocknote/core": "0.47.1",
|
||||
"@blocknote/mantine": "0.47.1",
|
||||
@@ -38,20 +38,20 @@
|
||||
"@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.35",
|
||||
"@fontsource-variable/material-symbols-outlined": "5.2.38",
|
||||
"@fontsource/material-icons": "5.2.7",
|
||||
"@gouvfr-lasuite/cunningham-react": "4.2.0",
|
||||
"@gouvfr-lasuite/integration": "1.0.3",
|
||||
"@gouvfr-lasuite/ui-kit": "0.19.6",
|
||||
"@gouvfr-lasuite/ui-kit": "0.19.10",
|
||||
"@hocuspocus/provider": "3.4.4",
|
||||
"@mantine/core": "8.3.14",
|
||||
"@mantine/hooks": "8.3.14",
|
||||
"@mantine/core": "8.3.17",
|
||||
"@mantine/hooks": "8.3.17",
|
||||
"@react-aria/live-announcer": "3.4.4",
|
||||
"@react-pdf/renderer": "4.3.1",
|
||||
"@sentry/nextjs": "10.38.0",
|
||||
"@sentry/nextjs": "10.43.0",
|
||||
"@tanstack/react-query": "5.90.21",
|
||||
"@tiptap/extensions": "*",
|
||||
"ai": "6.0.49",
|
||||
"ai": "6.0.128",
|
||||
"canvg": "4.0.3",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
@@ -59,28 +59,28 @@
|
||||
"emoji-datasource-apple": "16.0.0",
|
||||
"emoji-mart": "5.6.0",
|
||||
"emoji-regex": "10.6.0",
|
||||
"i18next": "25.8.12",
|
||||
"i18next": "25.8.18",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"idb": "8.0.3",
|
||||
"lodash": "4.17.23",
|
||||
"luxon": "3.7.2",
|
||||
"next": "16.1.7",
|
||||
"posthog-js": "1.347.2",
|
||||
"posthog-js": "1.360.2",
|
||||
"react": "*",
|
||||
"react-aria-components": "1.15.1",
|
||||
"react-aria-components": "1.16.0",
|
||||
"react-dom": "*",
|
||||
"react-dropzone": "15.0.0",
|
||||
"react-i18next": "16.5.4",
|
||||
"react-intersection-observer": "10.0.2",
|
||||
"react-i18next": "16.5.8",
|
||||
"react-intersection-observer": "10.0.3",
|
||||
"react-resizable-panels": "3.0.6",
|
||||
"react-select": "5.10.2",
|
||||
"styled-components": "6.3.9",
|
||||
"styled-components": "6.3.11",
|
||||
"use-debounce": "10.1.0",
|
||||
"uuid": "13.0.0",
|
||||
"y-protocols": "1.0.7",
|
||||
"yjs": "*",
|
||||
"zod": "3.25.28",
|
||||
"zustand": "5.0.11"
|
||||
"zod": "4.3.6",
|
||||
"zustand": "5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "8.1.0",
|
||||
@@ -89,26 +89,25 @@
|
||||
"@testing-library/jest-dom": "6.9.1",
|
||||
"@testing-library/react": "16.3.2",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/lodash": "4.17.23",
|
||||
"@types/lodash": "4.17.24",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"@vitejs/plugin-react": "5.1.4",
|
||||
"@vitejs/plugin-react": "6.0.1",
|
||||
"cross-env": "10.1.0",
|
||||
"dotenv": "17.3.1",
|
||||
"eslint-plugin-docs": "*",
|
||||
"fetch-mock": "9.11.0",
|
||||
"jsdom": "28.1.0",
|
||||
"jsdom": "29.0.0",
|
||||
"node-fetch": "2.7.0",
|
||||
"prettier": "3.8.1",
|
||||
"stylelint": "16.26.1",
|
||||
"stylelint-config-standard": "39.0.1",
|
||||
"stylelint-prettier": "5.0.3",
|
||||
"typescript": "*",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.0.18",
|
||||
"webpack": "5.105.2",
|
||||
"vitest": "4.1.0",
|
||||
"webpack": "5.105.4",
|
||||
"workbox-webpack-plugin": "7.1.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { Ref, forwardRef } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxType } from './Box';
|
||||
|
||||
export type BoxButtonType = BoxType & {
|
||||
export type BoxButtonType = Omit<BoxType, 'ref'> & {
|
||||
disabled?: boolean;
|
||||
ref?: Ref<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* Styleless button that extends the Box component.
|
||||
* Good to wrap around SVGs or other elements that need to be clickable.
|
||||
* Uses aria-disabled instead of native disabled to preserve keyboard focusability.
|
||||
* @param props - @see BoxType props
|
||||
* @param ref
|
||||
* @see Box
|
||||
@@ -22,8 +22,8 @@ export type BoxButtonType = BoxType & {
|
||||
* </BoxButton>
|
||||
* ```
|
||||
*/
|
||||
const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
|
||||
({ $css, ...props }, ref) => {
|
||||
const BoxButton = forwardRef<HTMLButtonElement, BoxButtonType>(
|
||||
({ $css, disabled, ...props }, ref) => {
|
||||
const theme = props.$theme || 'gray';
|
||||
const variation = props.$variation || 'primary';
|
||||
|
||||
@@ -31,16 +31,18 @@ const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
|
||||
<Box
|
||||
ref={ref}
|
||||
as="button"
|
||||
type="button"
|
||||
$background="none"
|
||||
$margin="none"
|
||||
$padding="none"
|
||||
$hasTransition
|
||||
aria-disabled={disabled || undefined}
|
||||
$css={css`
|
||||
cursor: ${props.disabled ? 'not-allowed' : 'pointer'};
|
||||
cursor: ${disabled ? 'not-allowed' : 'pointer'};
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
color: ${props.disabled &&
|
||||
color: ${disabled &&
|
||||
`var(--c--contextuals--content--semantic--disabled--primary)`};
|
||||
&:focus-visible {
|
||||
transition: none;
|
||||
@@ -53,11 +55,11 @@ const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
|
||||
`}
|
||||
{...props}
|
||||
className={`--docs--box-button ${props.className || ''}`}
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (props.disabled) {
|
||||
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
props.onClick?.(event);
|
||||
props.onClick?.(event as unknown as React.MouseEvent<HTMLDivElement>);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -93,7 +93,7 @@ export const DropButton = ({
|
||||
onOpenChangeHandler(true);
|
||||
}}
|
||||
aria-label={label}
|
||||
aria-haspopup="true"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={isLocalOpen}
|
||||
data-testid={testId}
|
||||
$css={css`
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
|
||||
export type DropdownMenuOption = {
|
||||
icon?: ReactNode;
|
||||
label: string;
|
||||
lang?: string;
|
||||
testId?: string;
|
||||
value?: string;
|
||||
callback?: () => void | Promise<unknown>;
|
||||
@@ -69,7 +70,10 @@ export const DropdownMenu = ({
|
||||
const [isOpen, setIsOpen] = useState(opened ?? false);
|
||||
const [focusedIndex, setFocusedIndex] = useState(-1);
|
||||
const blockButtonRef = useRef<HTMLDivElement>(null);
|
||||
const menuItemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||
const isSingleSelectable = options.some(
|
||||
(option) => option.isSelected !== undefined,
|
||||
);
|
||||
|
||||
const onOpenChange = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
@@ -110,10 +114,6 @@ export const DropdownMenu = ({
|
||||
[onOpenChange],
|
||||
);
|
||||
|
||||
const hasSelectable =
|
||||
selectedValues !== undefined ||
|
||||
options.some((option) => option.isSelected !== undefined);
|
||||
|
||||
if (disabled) {
|
||||
return children;
|
||||
}
|
||||
@@ -176,20 +176,25 @@ export const DropdownMenu = ({
|
||||
}
|
||||
const isDisabled = option.disabled !== undefined && option.disabled;
|
||||
const isFocused = index === focusedIndex;
|
||||
const ariaChecked = hasSelectable
|
||||
? option.isSelected ||
|
||||
selectedValues?.includes(option.value ?? '') ||
|
||||
false
|
||||
: undefined;
|
||||
const isSelected =
|
||||
option.isSelected === true ||
|
||||
(selectedValues?.includes(option.value ?? '') ?? false);
|
||||
const itemRole =
|
||||
selectedValues !== undefined
|
||||
? 'menuitemcheckbox'
|
||||
: isSingleSelectable
|
||||
? 'menuitemradio'
|
||||
: 'menuitem';
|
||||
const optionKey = option.value ?? option.testId ?? `option-${index}`;
|
||||
|
||||
return (
|
||||
<Fragment key={option.label}>
|
||||
<Fragment key={optionKey}>
|
||||
<BoxButton
|
||||
ref={(el) => {
|
||||
menuItemRefs.current[index] = el;
|
||||
}}
|
||||
role={hasSelectable ? 'menuitemradio' : 'menuitem'}
|
||||
aria-checked={ariaChecked}
|
||||
role={itemRole}
|
||||
aria-checked={itemRole === 'menuitem' ? undefined : isSelected}
|
||||
data-testid={option.testId}
|
||||
$direction="row"
|
||||
disabled={isDisabled}
|
||||
@@ -200,7 +205,6 @@ export const DropdownMenu = ({
|
||||
triggerOption(option);
|
||||
}}
|
||||
onKeyDown={keyboardAction(() => triggerOption(option))}
|
||||
key={option.label}
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
$background="var(--c--contextuals--background--surface--primary)"
|
||||
@@ -276,11 +280,10 @@ export const DropdownMenu = ({
|
||||
</Box>
|
||||
)}
|
||||
<Text $variation={isDisabled ? 'tertiary' : 'primary'}>
|
||||
{option.label}
|
||||
<span lang={option.lang}>{option.label}</span>
|
||||
</Text>
|
||||
</Box>
|
||||
{(option.isSelected ||
|
||||
selectedValues?.includes(option.value ?? '')) && (
|
||||
{isSelected && (
|
||||
<Icon
|
||||
iconName="check"
|
||||
$size="20px"
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('<DropdownMenu />', () => {
|
||||
expect(radios[2]).toHaveAttribute('aria-checked', 'false');
|
||||
});
|
||||
|
||||
test('renders menuitemradio role with aria-checked when selectedValues is provided', async () => {
|
||||
test('renders menuitemcheckbox role with aria-checked when selectedValues is provided', async () => {
|
||||
const optionsWithValues: DropdownMenuOption[] = [
|
||||
{ label: 'English', value: 'en', callback: vi.fn() },
|
||||
{ label: 'Français', value: 'fr', callback: vi.fn() },
|
||||
@@ -77,12 +77,12 @@ describe('<DropdownMenu />', () => {
|
||||
{ wrapper: AppWrapper },
|
||||
);
|
||||
|
||||
const radios = screen.getAllByRole('menuitemradio');
|
||||
expect(radios).toHaveLength(3);
|
||||
const checkboxes = screen.getAllByRole('menuitemcheckbox');
|
||||
expect(checkboxes).toHaveLength(3);
|
||||
|
||||
expect(radios[0]).toHaveAttribute('aria-checked', 'false');
|
||||
expect(radios[1]).toHaveAttribute('aria-checked', 'true');
|
||||
expect(radios[2]).toHaveAttribute('aria-checked', 'false');
|
||||
expect(checkboxes[0]).toHaveAttribute('aria-checked', 'false');
|
||||
expect(checkboxes[1]).toHaveAttribute('aria-checked', 'true');
|
||||
expect(checkboxes[2]).toHaveAttribute('aria-checked', 'false');
|
||||
});
|
||||
|
||||
test('trigger button has aria-haspopup and aria-expanded', async () => {
|
||||
@@ -94,7 +94,7 @@ describe('<DropdownMenu />', () => {
|
||||
);
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'Select language' });
|
||||
expect(trigger).toHaveAttribute('aria-haspopup', 'true');
|
||||
expect(trigger).toHaveAttribute('aria-haspopup', 'menu');
|
||||
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
await userEvent.click(trigger);
|
||||
|
||||
@@ -6,7 +6,7 @@ type UseDropdownKeyboardNavProps = {
|
||||
isOpen: boolean;
|
||||
focusedIndex: number;
|
||||
options: DropdownMenuOption[];
|
||||
menuItemRefs: RefObject<(HTMLDivElement | null)[]>;
|
||||
menuItemRefs: RefObject<(HTMLButtonElement | null)[]>;
|
||||
setFocusedIndex: (index: number) => void;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@ export const QuickSearchInput = ({
|
||||
$direction="row"
|
||||
$align="center"
|
||||
className="quick-search-input"
|
||||
$gap={spacingsTokens['2xs']}
|
||||
$gap={spacingsTokens['xxs']}
|
||||
$padding={{ horizontal: 'base', vertical: 'xxs' }}
|
||||
>
|
||||
<Icon iconName="search" $variation="secondary" aria-hidden="true" />
|
||||
@@ -62,6 +62,7 @@ export const QuickSearchInput = ({
|
||||
placeholder={placeholder ?? t('Search')}
|
||||
onValueChange={onFilter}
|
||||
maxLength={254}
|
||||
minLength={6}
|
||||
data-testid="quick-search-input"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -18,14 +18,15 @@ export const QuickSearchStyle = createGlobalStyle`
|
||||
[cmdk-input] {
|
||||
border: none;
|
||||
width: 100%;
|
||||
font-size: 17px;
|
||||
font-size: 16px;
|
||||
background: white;
|
||||
outline: none;
|
||||
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||
border-radius: var(--c--globals--spacings--0);
|
||||
font-family: var(--c--globals--font--families--base);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--c--globals--colors--gray-500);
|
||||
color: var(--c--contextuals--content--semantic--neutral--tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface ConfigResponse {
|
||||
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY?: boolean;
|
||||
CONVERSION_FILE_EXTENSIONS_ALLOWED: string[];
|
||||
CONVERSION_FILE_MAX_SIZE: number;
|
||||
CONVERSION_UPLOAD_ENABLED?: boolean;
|
||||
CRISP_WEBSITE_ID?: string;
|
||||
ENVIRONMENT: string;
|
||||
FRONTEND_CSS_URL?: string;
|
||||
|
||||
@@ -52,7 +52,7 @@ import {
|
||||
const AIMenu = BlockNoteAI?.AIMenu;
|
||||
const AIMenuController = BlockNoteAI?.AIMenuController;
|
||||
const useAI = BlockNoteAI?.useAI;
|
||||
const localesBNAI = BlockNoteAI?.localesAI;
|
||||
const localesBNAI = BlockNoteAI?.localesAI || {};
|
||||
import {
|
||||
InterlinkingLinkInlineContent,
|
||||
InterlinkingSearchInlineContent,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { useUpdateDoc } from '@/docs/doc-management/';
|
||||
import { KEY_LIST_DOC_VERSIONS } from '@/docs/doc-versioning';
|
||||
import { KEY_LIST_DOC_VERSIONS } from '@/docs/doc-versioning/api/useDocVersions';
|
||||
import { toBase64 } from '@/utils/string';
|
||||
import { isFirefox } from '@/utils/userAgent';
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('@/docs/doc-export/components/ModalExport', () => ({
|
||||
ModalExport: vi.fn(),
|
||||
}));
|
||||
|
||||
const originalEnv = process.env.NEXT_PUBLIC_PUBLISH_AS_MIT;
|
||||
|
||||
describe('useModuleExport', () => {
|
||||
@@ -16,12 +21,12 @@ describe('useModuleExport', () => {
|
||||
const Export = await import('@/features/docs/doc-export/');
|
||||
|
||||
expect(Export.default).toBeUndefined();
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
it('should load modules when NEXT_PUBLIC_PUBLISH_AS_MIT is false', async () => {
|
||||
process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = 'false';
|
||||
const Export = await import('@/features/docs/doc-export/');
|
||||
|
||||
expect(Export.default).toHaveProperty('ModalExport');
|
||||
}, 15000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import { improveHtmlAccessibility } from '../utils_html';
|
||||
|
||||
const parse = (html: string): Document => {
|
||||
const parser = new DOMParser();
|
||||
return parser.parseFromString(html, 'text/html');
|
||||
};
|
||||
|
||||
const bodyHtml = (doc: Document) =>
|
||||
doc.body.innerHTML.replace(/\s+/g, ' ').trim();
|
||||
|
||||
describe('improveHtmlAccessibility', () => {
|
||||
// Headings
|
||||
|
||||
describe('headings', () => {
|
||||
it('converts heading blocks without inner h tag to semantic h1–h6', () => {
|
||||
const doc = parse(`
|
||||
<div data-content-type="heading" data-level="3"><span>Title</span></div>
|
||||
`);
|
||||
|
||||
improveHtmlAccessibility(doc, 'Doc');
|
||||
|
||||
const h3 = doc.querySelector('h3');
|
||||
expect(h3).not.toBeNull();
|
||||
expect(h3!.textContent.trim()).toBe('Title');
|
||||
});
|
||||
|
||||
it('does not nest a second heading when BlockNote already outputs h1–h6', () => {
|
||||
const doc = parse(`
|
||||
<div data-content-type="heading" data-level="2">
|
||||
<h2 class="bn-inline-content">Section</h2>
|
||||
</div>
|
||||
`);
|
||||
|
||||
improveHtmlAccessibility(doc, 'Doc');
|
||||
|
||||
const h2 = doc.querySelectorAll('h2');
|
||||
expect(h2).toHaveLength(1);
|
||||
expect(h2[0].textContent).toBe('Section');
|
||||
expect(h2[0].className).toBe('bn-inline-content');
|
||||
});
|
||||
|
||||
it('uses data-level when inner heading tag has a different level', () => {
|
||||
const doc = parse(`
|
||||
<div data-content-type="heading" data-level="3">
|
||||
<h2 class="bn-inline-content">Mismatch</h2>
|
||||
</div>
|
||||
`);
|
||||
|
||||
improveHtmlAccessibility(doc, 'Doc');
|
||||
|
||||
expect(doc.querySelector('h3')).not.toBeNull();
|
||||
expect(doc.querySelector('h2')).toBeNull();
|
||||
});
|
||||
|
||||
it('inserts an h1 with the document title when none exists', () => {
|
||||
const doc = parse(`<p>Content</p>`);
|
||||
|
||||
improveHtmlAccessibility(doc, 'My Doc');
|
||||
|
||||
const h1 = doc.querySelector('h1');
|
||||
expect(h1).not.toBeNull();
|
||||
expect(h1!.id).toBe('doc-title');
|
||||
expect(h1!.textContent).toBe('My Doc');
|
||||
});
|
||||
|
||||
it('does not insert h1 when the document already has one', () => {
|
||||
const doc = parse(`
|
||||
<div data-content-type="heading" data-level="1"><span>Existing</span></div>
|
||||
`);
|
||||
|
||||
improveHtmlAccessibility(doc, 'Ignored');
|
||||
|
||||
expect(doc.querySelectorAll('h1')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
// Lists
|
||||
|
||||
describe('lists', () => {
|
||||
it('converts bullet list items to ul > li', () => {
|
||||
const doc = parse(`
|
||||
<div class="bn-block-group">
|
||||
<div class="bn-block-outer">
|
||||
<div data-content-type="bulletListItem"><span>A</span></div>
|
||||
</div>
|
||||
<div class="bn-block-outer">
|
||||
<div data-content-type="bulletListItem"><span>B</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
improveHtmlAccessibility(doc, 'Doc');
|
||||
|
||||
const items = doc.querySelectorAll('ul > li');
|
||||
expect(items).toHaveLength(2);
|
||||
expect(items[0].textContent).toBe('A');
|
||||
expect(items[1].textContent).toBe('B');
|
||||
});
|
||||
|
||||
it('converts numbered list items to ol > li', () => {
|
||||
const doc = parse(`
|
||||
<div class="bn-block-group">
|
||||
<div class="bn-block-outer">
|
||||
<div data-content-type="numberedListItem"><span>1st</span></div>
|
||||
</div>
|
||||
<div class="bn-block-outer">
|
||||
<div data-content-type="numberedListItem"><span>2nd</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
improveHtmlAccessibility(doc, 'Doc');
|
||||
|
||||
expect(doc.querySelectorAll('ol > li')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('splits lists when a heading sits between them', () => {
|
||||
const doc = parse(`
|
||||
<div class="bn-block-group">
|
||||
<div class="bn-block-outer">
|
||||
<div data-content-type="bulletListItem"><span>A</span></div>
|
||||
</div>
|
||||
<div class="bn-block-outer">
|
||||
<div data-content-type="bulletListItem"><span>B</span></div>
|
||||
</div>
|
||||
<div class="bn-block-outer">
|
||||
<div data-content-type="heading" data-level="2"><span>Next</span></div>
|
||||
</div>
|
||||
<div class="bn-block-outer">
|
||||
<div data-content-type="bulletListItem"><span>C</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
improveHtmlAccessibility(doc, 'Doc');
|
||||
|
||||
const uls = doc.querySelectorAll('ul');
|
||||
expect(uls).toHaveLength(2);
|
||||
expect(uls[0].querySelectorAll('li')).toHaveLength(2);
|
||||
expect(uls[1].querySelectorAll('li')).toHaveLength(1);
|
||||
|
||||
const html = bodyHtml(doc);
|
||||
expect(html.indexOf('<ul')).toBeLessThan(html.indexOf('<h2'));
|
||||
expect(html.indexOf('<h2')).toBeLessThan(
|
||||
html.indexOf('<ul', html.indexOf('<ul') + 1),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps consecutive same-type items in a single list', () => {
|
||||
const doc = parse(`
|
||||
<div class="bn-block-group">
|
||||
<div class="bn-block-outer">
|
||||
<div data-content-type="bulletListItem"><span>A</span></div>
|
||||
</div>
|
||||
<div class="bn-block-outer">
|
||||
<div data-content-type="bulletListItem"><span>B</span></div>
|
||||
</div>
|
||||
<div class="bn-block-outer">
|
||||
<div data-content-type="bulletListItem"><span>C</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
improveHtmlAccessibility(doc, 'Doc');
|
||||
|
||||
expect(doc.querySelectorAll('ul')).toHaveLength(1);
|
||||
expect(doc.querySelectorAll('ul > li')).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
// Quotes
|
||||
|
||||
it('converts quote blocks to blockquote', () => {
|
||||
const doc = parse(`
|
||||
<div data-content-type="quote"><span>Wise words</span></div>
|
||||
`);
|
||||
|
||||
improveHtmlAccessibility(doc, 'Doc');
|
||||
|
||||
const bq = doc.querySelector('blockquote');
|
||||
expect(bq).not.toBeNull();
|
||||
expect(bq!.textContent).toBe('Wise words');
|
||||
});
|
||||
|
||||
// Callouts
|
||||
|
||||
it('converts callout blocks to aside with role="note"', () => {
|
||||
const doc = parse(`
|
||||
<div data-content-type="callout"><span>Note</span></div>
|
||||
`);
|
||||
|
||||
improveHtmlAccessibility(doc, 'Doc');
|
||||
|
||||
const aside = doc.querySelector('aside');
|
||||
expect(aside).not.toBeNull();
|
||||
expect(aside!.getAttribute('role')).toBe('note');
|
||||
});
|
||||
|
||||
// Checklists
|
||||
|
||||
it('wraps check list items in a ul with role="list" and adds aria-checked', () => {
|
||||
const doc = parse(`
|
||||
<div>
|
||||
<div data-content-type="checkListItem">
|
||||
<input type="checkbox" />
|
||||
<span>Todo</span>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
improveHtmlAccessibility(doc, 'Doc');
|
||||
|
||||
const ul = doc.querySelector('ul.checklist');
|
||||
expect(ul).not.toBeNull();
|
||||
expect(ul!.getAttribute('role')).toBe('list');
|
||||
expect(doc.querySelector('input')!.getAttribute('aria-checked')).toBe(
|
||||
'false',
|
||||
);
|
||||
});
|
||||
|
||||
// Code blocks
|
||||
|
||||
it('converts code blocks to pre > code and preserves data attributes', () => {
|
||||
const doc = parse(`
|
||||
<div data-content-type="codeBlock" class="hl-theme" data-language="js"><span>const x = 1;</span></div>
|
||||
`);
|
||||
|
||||
improveHtmlAccessibility(doc, 'Doc');
|
||||
|
||||
const pre = doc.querySelector('pre');
|
||||
expect(pre).not.toBeNull();
|
||||
expect(pre!.className).toBe('hl-theme');
|
||||
expect(pre!.getAttribute('data-language')).toBe('js');
|
||||
expect(pre!.querySelector('code')!.textContent.trim()).toBe('const x = 1;');
|
||||
});
|
||||
|
||||
// Images
|
||||
|
||||
it('adds empty alt to images without one', () => {
|
||||
const doc = parse(`<img src="photo.png" />`);
|
||||
|
||||
improveHtmlAccessibility(doc, 'Doc');
|
||||
|
||||
expect(doc.querySelector('img')!.getAttribute('alt')).toBe('');
|
||||
});
|
||||
|
||||
it('does not overwrite an existing alt', () => {
|
||||
const doc = parse(`<img src="photo.png" alt="A photo" />`);
|
||||
|
||||
improveHtmlAccessibility(doc, 'Doc');
|
||||
|
||||
expect(doc.querySelector('img')!.getAttribute('alt')).toBe('A photo');
|
||||
});
|
||||
|
||||
// Article wrapper
|
||||
|
||||
it('wraps body in article with role="document"', () => {
|
||||
const doc = parse(`<p>Hello</p>`);
|
||||
|
||||
improveHtmlAccessibility(doc, 'Doc');
|
||||
|
||||
const article = doc.querySelector('article');
|
||||
expect(article).not.toBeNull();
|
||||
expect(article!.getAttribute('role')).toBe('document');
|
||||
expect(article!.getAttribute('aria-labelledby')).toBe('doc-title');
|
||||
});
|
||||
});
|
||||
@@ -145,8 +145,20 @@ export const improveHtmlAccessibility = (
|
||||
headingBlocks.forEach((block) => {
|
||||
const rawLevel = Number(block.getAttribute('data-level')) || 1;
|
||||
const level = Math.min(Math.max(rawLevel, 1), 6);
|
||||
const heading = parsedDocument.createElement(`h${level}`);
|
||||
moveChildNodes(block, heading);
|
||||
const tag = `h${level}`;
|
||||
|
||||
const existingHeading = block.querySelector('h1, h2, h3, h4, h5, h6');
|
||||
const heading = parsedDocument.createElement(tag);
|
||||
|
||||
if (existingHeading) {
|
||||
if (existingHeading.className) {
|
||||
heading.className = existingHeading.className;
|
||||
}
|
||||
moveChildNodes(existingHeading, heading);
|
||||
} else {
|
||||
moveChildNodes(block, heading);
|
||||
}
|
||||
|
||||
block.replaceWith(heading);
|
||||
});
|
||||
|
||||
@@ -178,10 +190,11 @@ export const improveHtmlAccessibility = (
|
||||
listItem: HTMLElement;
|
||||
contentType: string;
|
||||
level: number;
|
||||
blockOuterIndex: number;
|
||||
}
|
||||
|
||||
const listItemsInfo: ListItemInfo[] = [];
|
||||
allBlockOuters.forEach((blockOuter) => {
|
||||
allBlockOuters.forEach((blockOuter, index) => {
|
||||
const listItem = blockOuter.querySelector<HTMLElement>(listItemSelector);
|
||||
if (listItem) {
|
||||
const contentType = listItem.getAttribute('data-content-type');
|
||||
@@ -192,6 +205,7 @@ export const improveHtmlAccessibility = (
|
||||
listItem,
|
||||
contentType,
|
||||
level,
|
||||
blockOuterIndex: index,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -206,13 +220,20 @@ export const improveHtmlAccessibility = (
|
||||
const isBullet = contentType === 'bulletListItem';
|
||||
const listTag = isBullet ? 'ul' : 'ol';
|
||||
|
||||
// Check if previous item continues the same list (same type and level)
|
||||
// Check if previous item continues the same list (same type, level, and
|
||||
// no non-list block between them in the DOM : e.g. a heading separates lists).
|
||||
const previousInfo = idx > 0 ? listItemsInfo[idx - 1] : null;
|
||||
const isAdjacentBlock =
|
||||
previousInfo && info.blockOuterIndex === previousInfo.blockOuterIndex + 1;
|
||||
const continuesPreviousList =
|
||||
previousInfo &&
|
||||
isAdjacentBlock &&
|
||||
previousInfo.contentType === contentType &&
|
||||
previousInfo.level === level;
|
||||
|
||||
if (previousInfo && !isAdjacentBlock) {
|
||||
listStack.length = 0;
|
||||
}
|
||||
|
||||
// Find or create the appropriate list
|
||||
let targetList: HTMLElement | null = null;
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { afterAll, beforeEach, describe, expect, vi } from 'vitest';
|
||||
|
||||
import { AppWrapper } from '@/tests/utils';
|
||||
@@ -40,17 +38,11 @@ describe('DocToolBox - Licence', () => {
|
||||
render(<DocToolBox doc={doc as any} />, {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
const optionsButton = await screen.findByLabelText('Export the document');
|
||||
await userEvent.click(optionsButton);
|
||||
|
||||
// Wait for the export modal to be visible, then assert on its content text.
|
||||
await screen.findByTestId('modal-export-title');
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Export your document to print or download in .docx, .odt, .pdf or .html(zip) format.',
|
||||
),
|
||||
await screen.findByLabelText('Export the document'),
|
||||
).toBeInTheDocument();
|
||||
}, 10000);
|
||||
}, 15000);
|
||||
|
||||
test('The export button is not rendered when MIT version is activated', async () => {
|
||||
process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = 'true';
|
||||
@@ -68,5 +60,5 @@ describe('DocToolBox - Licence', () => {
|
||||
expect(
|
||||
screen.queryByLabelText('Export the document'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Button, useModal } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
@@ -39,7 +38,6 @@ import {
|
||||
useDocUtils,
|
||||
useDuplicateDoc,
|
||||
} from '@/docs/doc-management';
|
||||
import { KEY_LIST_DOC_VERSIONS } from '@/docs/doc-versioning';
|
||||
import { useFocusStore, useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useCopyCurrentEditorToClipboard } from '../hooks/useCopyCurrentEditorToClipboard';
|
||||
@@ -88,7 +86,6 @@ interface DocToolBoxProps {
|
||||
export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
const { t } = useTranslation();
|
||||
const treeContext = useTreeContext<Doc>();
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
const { isChild, isTopRoot } = useDocUtils(doc);
|
||||
|
||||
@@ -114,16 +111,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC, KEY_LIST_FAVORITE_DOC],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (selectHistoryModal.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
void queryClient.resetQueries({
|
||||
queryKey: [KEY_LIST_DOC_VERSIONS],
|
||||
});
|
||||
}, [selectHistoryModal.isOpen, queryClient]);
|
||||
|
||||
// Emoji Management
|
||||
const { emoji } = getEmojiAndTitle(doc.title ?? '');
|
||||
const { updateDocEmoji } = useDocTitleUpdate();
|
||||
@@ -131,13 +118,13 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
const options: DropdownMenuOption[] = [
|
||||
{
|
||||
label: t('Share'),
|
||||
icon: <GroupSVG width={24} height={24} />,
|
||||
icon: <GroupSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: modalShare.open,
|
||||
show: isSmallMobile,
|
||||
},
|
||||
{
|
||||
label: t('Export'),
|
||||
icon: <DownloadSVG width={24} height={24} />,
|
||||
icon: <DownloadSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
setIsModalExportOpen(true);
|
||||
},
|
||||
@@ -146,9 +133,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
{
|
||||
label: doc.is_favorite ? t('Unpin') : t('Pin'),
|
||||
icon: doc.is_favorite ? (
|
||||
<KeepOffSVG width={24} height={24} />
|
||||
<KeepOffSVG width={24} height={24} aria-hidden="true" />
|
||||
) : (
|
||||
<KeepSVG width={24} height={24} />
|
||||
<KeepSVG width={24} height={24} aria-hidden="true" />
|
||||
),
|
||||
callback: () => {
|
||||
if (doc.is_favorite) {
|
||||
@@ -161,7 +148,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Version history'),
|
||||
icon: <HistorySVG width={24} height={24} />,
|
||||
icon: <HistorySVG width={24} height={24} aria-hidden="true" />,
|
||||
disabled: !doc.abilities.versions_list,
|
||||
callback: () => {
|
||||
selectHistoryModal.open();
|
||||
@@ -171,7 +158,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Remove emoji'),
|
||||
icon: <RemoveEmojiSVG width={24} height={24} />,
|
||||
icon: <RemoveEmojiSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
updateDocEmoji(doc.id, doc.title ?? '', '');
|
||||
},
|
||||
@@ -180,7 +167,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Add emoji'),
|
||||
icon: <AddEmojiSVG width={24} height={24} />,
|
||||
icon: <AddEmojiSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
updateDocEmoji(doc.id, doc.title ?? '', '📄');
|
||||
},
|
||||
@@ -189,12 +176,12 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Copy link'),
|
||||
icon: <AddLinkSVG width={24} height={24} />,
|
||||
icon: <AddLinkSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: copyDocLink,
|
||||
},
|
||||
{
|
||||
label: t('Copy as {{format}}', { format: 'Markdown' }),
|
||||
icon: <MarkdownCopySVG width={24} height={24} />,
|
||||
icon: <MarkdownCopySVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
void copyCurrentEditorToClipboard('markdown');
|
||||
},
|
||||
@@ -202,7 +189,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Duplicate'),
|
||||
icon: <ContentCopySVG width={24} height={24} />,
|
||||
icon: <ContentCopySVG width={24} height={24} aria-hidden="true" />,
|
||||
disabled: !doc.abilities.duplicate,
|
||||
callback: () => {
|
||||
duplicateDoc({
|
||||
@@ -215,7 +202,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
},
|
||||
{
|
||||
label: isChild ? t('Delete sub-document') : t('Delete document'),
|
||||
icon: <DeleteSVG width={24} height={24} />,
|
||||
icon: <DeleteSVG width={24} height={24} aria-hidden="true" />,
|
||||
disabled: !doc.abilities.destroy,
|
||||
callback: () => {
|
||||
setIsModalRemoveOpen(true);
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useEditorStore } from '../../doc-editor';
|
||||
import { useEditorStore } from '@/docs/doc-editor/stores/useEditorStore';
|
||||
|
||||
export const useCopyCurrentEditorToClipboard = () => {
|
||||
const { editor } = useEditorStore();
|
||||
@@ -21,8 +21,8 @@ export const useCopyCurrentEditorToClipboard = () => {
|
||||
try {
|
||||
const editorContentFormatted =
|
||||
asFormat === 'html'
|
||||
? await editor.blocksToHTMLLossy()
|
||||
: await editor.blocksToMarkdownLossy();
|
||||
? editor.blocksToHTMLLossy()
|
||||
: editor.blocksToMarkdownLossy();
|
||||
await navigator.clipboard.writeText(editorContentFormatted);
|
||||
const successMessage =
|
||||
asFormat === 'markdown'
|
||||
|
||||
@@ -44,7 +44,7 @@ export function useUpdateDoc(queryConfig?: UseUpdateDoc) {
|
||||
...queryConfig,
|
||||
onSuccess: (data, variables, onMutateResult, context) => {
|
||||
queryConfig?.listInvalidQueries?.forEach((queryKey) => {
|
||||
void queryClient.invalidateQueries({
|
||||
void queryClient.resetQueries({
|
||||
queryKey: [queryKey],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ export const DocIcon = ({
|
||||
const { t } = useTranslation();
|
||||
const { addLastFocus, restoreFocus } = useFocusStore();
|
||||
|
||||
const iconRef = useRef<HTMLDivElement>(null);
|
||||
const iconRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const [openEmojiPicker, setOpenEmojiPicker] = useState<boolean>(false);
|
||||
const [pickerPosition, setPickerPosition] = useState<{
|
||||
|
||||
@@ -14,6 +14,11 @@ vi.mock('@/stores', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@gouvfr-lasuite/ui-kit', async () => ({
|
||||
...(await vi.importActual('@gouvfr-lasuite/ui-kit')),
|
||||
useTreeContext: () => null,
|
||||
}));
|
||||
|
||||
describe('useDocTitleUpdate', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Role } from '../types';
|
||||
import { DocDefaultFilter, Role } from '../types';
|
||||
|
||||
export const useTrans = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -12,11 +12,22 @@ export const useTrans = () => {
|
||||
[Role.OWNER]: t('Owner'),
|
||||
};
|
||||
|
||||
const translatedFilters = {
|
||||
[DocDefaultFilter.ALL_DOCS]: t('All docs'),
|
||||
[DocDefaultFilter.MY_DOCS]: t('My docs'),
|
||||
[DocDefaultFilter.SHARED_WITH_ME]: t('Shared with me'),
|
||||
[DocDefaultFilter.TRASHBIN]: t('Trashbin'),
|
||||
};
|
||||
|
||||
return {
|
||||
transRole: (role: Role) => {
|
||||
return translatedRoles[role];
|
||||
},
|
||||
transFilter: (filter: DocDefaultFilter) => {
|
||||
return translatedFilters[filter];
|
||||
},
|
||||
untitledDocument: t('Untitled document'),
|
||||
translatedRoles,
|
||||
translatedFilters,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -124,7 +124,8 @@ export const DocShareAddMemberList = ({
|
||||
$scope="surface"
|
||||
$theme="tertiary"
|
||||
$variation=""
|
||||
$border="1px solid var(--c--contextuals--border--semantic--contextual--primary)"
|
||||
$border="1px solid var(--c--contextuals--border--surface--primary)"
|
||||
$margin={{ bottom: 'sm' }}
|
||||
>
|
||||
<Box
|
||||
$direction="row"
|
||||
|
||||
@@ -289,7 +289,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
/>
|
||||
)}
|
||||
{showMemberSection && isRootDoc && (
|
||||
<Box $padding={{ horizontal: 'base' }}>
|
||||
<Box $padding={{ horizontal: 'base', top: 'base' }}>
|
||||
<QuickSearchGroupAccessRequest doc={doc} />
|
||||
<QuickSearchGroupInvitation doc={doc} />
|
||||
<QuickSearchGroupMember doc={doc} />
|
||||
@@ -301,6 +301,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
searchUsersRawData={searchUsersQuery.data}
|
||||
onSelect={onSelect}
|
||||
userQuery={userQuery}
|
||||
minLength={API_USERS_SEARCH_QUERY_MIN_LENGTH}
|
||||
/>
|
||||
)}
|
||||
</QuickSearch>
|
||||
@@ -321,14 +322,35 @@ interface QuickSearchInviteInputSectionProps {
|
||||
onSelect: (usr: User) => void;
|
||||
searchUsersRawData: User[] | undefined;
|
||||
userQuery: string;
|
||||
minLength: number;
|
||||
}
|
||||
|
||||
const QuickSearchInviteInputSection = ({
|
||||
onSelect,
|
||||
searchUsersRawData,
|
||||
userQuery,
|
||||
minLength,
|
||||
}: QuickSearchInviteInputSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
const hint = useMemo(() => {
|
||||
if (userQuery.length < minLength) {
|
||||
return t('Type at least {{minLength}} characters to display user names', {
|
||||
minLength,
|
||||
});
|
||||
}
|
||||
if (isValidEmail(userQuery)) {
|
||||
return t('Choose the email');
|
||||
}
|
||||
if (!searchUsersRawData?.length) {
|
||||
return t('No results. Type a full email address to invite someone.');
|
||||
}
|
||||
|
||||
return t('Choose a user');
|
||||
}, [minLength, searchUsersRawData?.length, t, userQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
announce(hint, 'polite');
|
||||
}, [hint]);
|
||||
|
||||
const searchUserData: QuickSearchData<User> = useMemo(() => {
|
||||
const users = searchUsersRawData || [];
|
||||
@@ -347,7 +369,7 @@ const QuickSearchInviteInputSection = ({
|
||||
);
|
||||
|
||||
return {
|
||||
groupName: t('Search user result'),
|
||||
groupName: hint,
|
||||
elements: users,
|
||||
endActions:
|
||||
isEmail && !hasEmailInUsers
|
||||
@@ -359,12 +381,12 @@ const QuickSearchInviteInputSection = ({
|
||||
]
|
||||
: undefined,
|
||||
};
|
||||
}, [onSelect, searchUsersRawData, t, userQuery]);
|
||||
}, [searchUsersRawData, userQuery, hint, onSelect]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
aria-label={t('List search user result card')}
|
||||
$padding={{ horizontal: 'base', bottom: '3xs' }}
|
||||
$padding={{ horizontal: 'base', bottom: '3xs', top: 'base' }}
|
||||
>
|
||||
<QuickSearchGroup
|
||||
group={searchUserData}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import {
|
||||
Spinner,
|
||||
TreeViewDataType,
|
||||
TreeViewItem,
|
||||
TreeViewNodeProps,
|
||||
TreeViewNodeTypeEnum,
|
||||
useTreeContext,
|
||||
} from '@gouvfr-lasuite/ui-kit';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
@@ -20,6 +22,8 @@ import {
|
||||
import { useLeftPanelStore } from '@/features/left-panel';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { isDocNode } from '../utils';
|
||||
|
||||
import SubPageIcon from './../assets/sub-page-logo.svg';
|
||||
import { DocTreeItemActions } from './DocTreeItemActions';
|
||||
|
||||
@@ -34,6 +38,65 @@ const ItemTextCss = css`
|
||||
`;
|
||||
|
||||
export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
||||
if (props.node.data.value.nodeType === TreeViewNodeTypeEnum.VIEW_MORE) {
|
||||
return <DocSubPageLoadMore {...props} />;
|
||||
}
|
||||
|
||||
if (!isDocNode(props.node.data.value)) {
|
||||
return <TreeViewItem {...props} />;
|
||||
}
|
||||
|
||||
return <DocSubPageItemContent {...props} />;
|
||||
};
|
||||
|
||||
const DocSubPageLoadMore = (props: TreeViewNodeProps<Doc>) => {
|
||||
const treeContext = useTreeContext<Doc>();
|
||||
const loaderRef = useRef<HTMLDivElement>(null);
|
||||
const inFlightRef = useRef<boolean>(false);
|
||||
|
||||
/**
|
||||
* Use IntersectionObserver to trigger loading more children when the "Load More" item comes into view.
|
||||
* This allows for infinite scrolling of child nodes without needing a "Load More" button click.
|
||||
* The observer is disconnected when the component unmounts to prevent memory leaks.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const el = loaderRef.current;
|
||||
const parentKey = props.node.data.parentKey;
|
||||
if (!el || !parentKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (!entry.isIntersecting || inFlightRef.current) {
|
||||
return;
|
||||
}
|
||||
inFlightRef.current = true;
|
||||
void treeContext?.treeData.handleLoadChildren(parentKey).finally(() => {
|
||||
inFlightRef.current = false;
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1 },
|
||||
);
|
||||
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={loaderRef}
|
||||
$align="center"
|
||||
$justify="center"
|
||||
$padding={{ vertical: 'xs' }}
|
||||
>
|
||||
<Spinner size="sm" />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const DocSubPageItemContent = (props: TreeViewNodeProps<Doc>) => {
|
||||
const doc = props.node.data.value as Doc;
|
||||
const treeContext = useTreeContext<Doc>();
|
||||
const { untitledDocument } = useTrans();
|
||||
@@ -96,7 +159,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
||||
const ariaLabel = docTitle;
|
||||
const isDisabled = !!doc.deleted_at;
|
||||
const actionsRef = useRef<HTMLDivElement>(null);
|
||||
const buttonOptionRef = useRef<HTMLDivElement | null>(null);
|
||||
const buttonOptionRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
|
||||
@@ -22,7 +22,7 @@ import { TreeSkeleton } from '@/features/skeletons/components/TreeSkeleton';
|
||||
|
||||
import { CLASS_DOC_TITLE } from '../../doc-header';
|
||||
import { KEY_DOC_TREE, useDocTree } from '../api/useDocTree';
|
||||
import { findIndexInTree } from '../utils';
|
||||
import { findIndexInTree, isDocNode } from '../utils';
|
||||
|
||||
import { DocSubPageItem } from './DocSubPageItem';
|
||||
import { DocTreeItemActions } from './DocTreeItemActions';
|
||||
@@ -44,7 +44,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
||||
treeContext?.treeData.selectedNode?.id === treeContext.root.id;
|
||||
const rootItemRef = useRef<HTMLDivElement>(null);
|
||||
const rootActionsRef = useRef<HTMLDivElement>(null);
|
||||
const rootButtonOptionRef = useRef<HTMLDivElement | null>(null);
|
||||
const rootButtonOptionRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -406,15 +406,17 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
||||
undefined
|
||||
}
|
||||
canDrop={({ parentNode }) => {
|
||||
const parentDoc = parentNode?.data.value as Doc;
|
||||
if (!parentDoc) {
|
||||
const parentValue = parentNode?.data.value;
|
||||
if (!parentValue || !isDocNode(parentValue)) {
|
||||
return currentDoc.abilities.move && isDesktop;
|
||||
}
|
||||
return parentDoc.abilities.move && isDesktop;
|
||||
return parentValue.abilities.move && isDesktop;
|
||||
}}
|
||||
canDrag={(node) => {
|
||||
const doc = node.value as Doc;
|
||||
return doc.abilities.move && isDesktop;
|
||||
if (!isDocNode(node.value)) {
|
||||
return false;
|
||||
}
|
||||
return node.value.abilities.move && isDesktop;
|
||||
}}
|
||||
rootNodeId={treeContext.root.id}
|
||||
renderNode={DocSubPageItem}
|
||||
|
||||
@@ -33,7 +33,7 @@ type DocTreeItemActionsProps = {
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
parentId?: string | null;
|
||||
actionsRef?: React.RefObject<HTMLDivElement | null>;
|
||||
buttonOptionRef?: React.RefObject<HTMLDivElement | null>;
|
||||
buttonOptionRef?: React.RefObject<HTMLButtonElement | null>;
|
||||
};
|
||||
|
||||
export const DocTreeItemActions = ({
|
||||
@@ -48,7 +48,7 @@ export const DocTreeItemActions = ({
|
||||
}: DocTreeItemActionsProps) => {
|
||||
const internalActionsRef = useRef<HTMLDivElement | null>(null);
|
||||
const targetActionsRef = actionsRef ?? internalActionsRef;
|
||||
const internalButtonRef = useRef<HTMLDivElement | null>(null);
|
||||
const internalButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const targetButtonRef = buttonOptionRef ?? internalButtonRef;
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
import { TreeDataItem, TreeViewDataType } from '@gouvfr-lasuite/ui-kit';
|
||||
import {
|
||||
TreeDataItem,
|
||||
TreeViewDataType,
|
||||
TreeViewNodeTypeEnum,
|
||||
} from '@gouvfr-lasuite/ui-kit';
|
||||
|
||||
import { Doc } from '../doc-management';
|
||||
|
||||
/**
|
||||
* Type guard to check if a tree node value is a Doc (as opposed to a
|
||||
* ui-kit synthetic node like VIEW_MORE, SEPARATOR, TITLE, or SIMPLE_NODE).
|
||||
*/
|
||||
export const isDocNode = (
|
||||
value: TreeViewDataType<Doc>,
|
||||
): value is TreeViewDataType<Doc> & Doc => {
|
||||
return !value.nodeType || value.nodeType === TreeViewNodeTypeEnum.NODE;
|
||||
};
|
||||
|
||||
export const subPageToTree = (children: Doc[]): TreeViewDataType<Doc>[] => {
|
||||
children.forEach((child) => {
|
||||
child.childrenCount = child.numchild ?? 0;
|
||||
|
||||
@@ -4,9 +4,12 @@ import { useEffect, useState } from 'react';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { Box, Text, TextErrors } from '@/components';
|
||||
import { BlockNoteReader, DocEditorContainer } from '@/docs/doc-editor/';
|
||||
import { BlockNoteReader } from '@/docs/doc-editor/components/BlockNoteEditor';
|
||||
import { DocEditorContainer } from '@/docs/doc-editor/components/DocEditor';
|
||||
import { Doc, base64ToBlocknoteXmlFragment } from '@/docs/doc-management';
|
||||
import { Versions, useDocVersion } from '@/docs/doc-versioning/';
|
||||
|
||||
import { useDocVersion } from '../api/useDocVersion';
|
||||
import { Versions } from '../types';
|
||||
|
||||
import { DocVersionHeader } from './DocVersionHeader';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Doc } from '../doc-management';
|
||||
import { Doc } from '../doc-management/types';
|
||||
|
||||
export interface APIListVersions {
|
||||
count: number;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { DndContext, DragOverlay, Modifier } from '@dnd-kit/core';
|
||||
import {
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
Modifier,
|
||||
UniqueIdentifier,
|
||||
} from '@dnd-kit/core';
|
||||
import { getEventCoordinates } from '@dnd-kit/utilities';
|
||||
import { useModal } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { TreeViewMoveModeEnum } from '@gouvfr-lasuite/ui-kit';
|
||||
@@ -107,6 +112,59 @@ export const DraggableDocGridContentList = ({
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dndAccessibility = useMemo(
|
||||
() => ({
|
||||
screenReaderInstructions: {
|
||||
draggable: t(
|
||||
'To pick up a draggable item, press space or enter. While dragging, use the arrow keys to move the item. Press space or enter again to drop the item in its new position, or press escape to cancel.',
|
||||
),
|
||||
},
|
||||
announcements: {
|
||||
onDragStart({ active }: { active: { id: UniqueIdentifier } }) {
|
||||
return t('Picked up document {{id}}.', { id: active.id });
|
||||
},
|
||||
onDragOver({
|
||||
active,
|
||||
over,
|
||||
}: {
|
||||
active: { id: UniqueIdentifier };
|
||||
over: { id: UniqueIdentifier } | null;
|
||||
}) {
|
||||
if (over) {
|
||||
return t('Document {{activeId}} is over document {{overId}}.', {
|
||||
activeId: active.id,
|
||||
overId: over.id,
|
||||
});
|
||||
}
|
||||
return t('Document {{id}} is no longer over a droppable area.', {
|
||||
id: active.id,
|
||||
});
|
||||
},
|
||||
onDragEnd({
|
||||
active,
|
||||
over,
|
||||
}: {
|
||||
active: { id: UniqueIdentifier };
|
||||
over: { id: UniqueIdentifier } | null;
|
||||
}) {
|
||||
if (over) {
|
||||
return t(
|
||||
'Document {{activeId}} was dropped over document {{overId}}.',
|
||||
{ activeId: active.id, overId: over.id },
|
||||
);
|
||||
}
|
||||
return t('Document {{id}} was dropped.', { id: active.id });
|
||||
},
|
||||
onDragCancel({ active }: { active: { id: UniqueIdentifier } }) {
|
||||
return t('Dragging was cancelled. Document {{id}} was dropped.', {
|
||||
id: active.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
const overlayText = useMemo(() => {
|
||||
if (!canDrag) {
|
||||
return t('You must be the owner to move the document');
|
||||
@@ -147,6 +205,7 @@ export const DraggableDocGridContentList = ({
|
||||
modifiers={[snapToTopLeft]}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
accessibility={dndAccessibility}
|
||||
>
|
||||
{docs.map((doc) => (
|
||||
<DraggableDocGridItem
|
||||
|
||||
@@ -44,6 +44,7 @@ export const DocsGrid = ({
|
||||
getInputProps,
|
||||
open,
|
||||
isPending: isImportPending,
|
||||
isEnabled: isImportEnabled,
|
||||
} = useImport({
|
||||
onDragOver: (dragOver: boolean) => {
|
||||
setIsDragOver(dragOver);
|
||||
@@ -51,9 +52,10 @@ export const DocsGrid = ({
|
||||
});
|
||||
|
||||
const withUpload =
|
||||
!target ||
|
||||
target === DocDefaultFilter.ALL_DOCS ||
|
||||
target === DocDefaultFilter.MY_DOCS;
|
||||
(!target ||
|
||||
target === DocDefaultFilter.ALL_DOCS ||
|
||||
target === DocDefaultFilter.MY_DOCS) &&
|
||||
isImportEnabled;
|
||||
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { flexLeft, flexRight } = useResponsiveDocGrid();
|
||||
@@ -140,36 +142,30 @@ export const DocsGrid = ({
|
||||
$overflow="auto"
|
||||
$padding={{ vertical: 'sm', horizontal: isDesktop ? 'md' : 'xs' }}
|
||||
>
|
||||
<Box role="grid" aria-label={t('Documents grid')}>
|
||||
<Box role="rowgroup">
|
||||
<Box
|
||||
$direction="row"
|
||||
$padding={{ horizontal: 'xs' }}
|
||||
$gap="10px"
|
||||
data-testid="docs-grid-header"
|
||||
role="row"
|
||||
>
|
||||
<Box $flex={flexLeft} $padding="3xs" role="columnheader">
|
||||
<Text $size="xs" $variation="secondary" $weight="500">
|
||||
{t('Name')}
|
||||
<Box aria-label={t('Documents grid')}>
|
||||
<Box
|
||||
$direction="row"
|
||||
$padding={{ horizontal: 'xs' }}
|
||||
$gap="10px"
|
||||
data-testid="docs-grid-header"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Box $flex={flexLeft} $padding="3xs">
|
||||
<Text $size="xs" $variation="secondary" $weight="500">
|
||||
{t('Name')}
|
||||
</Text>
|
||||
</Box>
|
||||
{isDesktop && (
|
||||
<Box $flex={flexRight} $padding={{ vertical: '3xs' }}>
|
||||
<Text $size="xs" $weight="500" $variation="secondary">
|
||||
{DocDefaultFilter.TRASHBIN === target
|
||||
? t('Days remaining')
|
||||
: t('Updated at')}
|
||||
</Text>
|
||||
</Box>
|
||||
{isDesktop && (
|
||||
<Box
|
||||
$flex={flexRight}
|
||||
$padding={{ vertical: '3xs' }}
|
||||
role="columnheader"
|
||||
>
|
||||
<Text $size="xs" $weight="500" $variation="secondary">
|
||||
{DocDefaultFilter.TRASHBIN === target
|
||||
? t('Days remaining')
|
||||
: t('Updated at')}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box role="rowgroup">
|
||||
<Box role="list">
|
||||
<DocGridContentList docs={docs} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -72,9 +72,9 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
||||
{
|
||||
label: doc.is_favorite ? t('Unpin') : t('Pin'),
|
||||
icon: doc.is_favorite ? (
|
||||
<KeepOffSVG width={24} height={24} />
|
||||
<KeepOffSVG width={24} height={24} aria-hidden="true" />
|
||||
) : (
|
||||
<KeepSVG width={24} height={24} />
|
||||
<KeepSVG width={24} height={24} aria-hidden="true" />
|
||||
),
|
||||
callback: () => {
|
||||
if (doc.is_favorite) {
|
||||
@@ -88,7 +88,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Share'),
|
||||
icon: <GroupSVG width={24} height={24} />,
|
||||
icon: <GroupSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
shareModal.open();
|
||||
},
|
||||
@@ -97,7 +97,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Move into a doc'),
|
||||
icon: <DocMoveInSVG width={24} height={24} />,
|
||||
icon: <DocMoveInSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
importModal.open();
|
||||
},
|
||||
@@ -106,7 +106,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Duplicate'),
|
||||
icon: <ContentCopySVG width={24} height={24} />,
|
||||
icon: <ContentCopySVG width={24} height={24} aria-hidden="true" />,
|
||||
disabled: !doc.abilities.duplicate,
|
||||
callback: () => {
|
||||
duplicateDoc({
|
||||
@@ -119,7 +119,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Delete'),
|
||||
icon: <DeleteSVG width={24} height={24} />,
|
||||
icon: <DeleteSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => deleteModal.open(),
|
||||
disabled: !doc.abilities.destroy,
|
||||
testId: `docs-grid-actions-remove-${doc.id}`,
|
||||
|
||||
@@ -32,6 +32,7 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { flexLeft, flexRight } = useResponsiveDocGrid();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const dateToDisplay = useDateToDisplay(doc, isInTrashbin);
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
@@ -46,7 +47,7 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
||||
$direction="row"
|
||||
$width="100%"
|
||||
$align="center"
|
||||
role="row"
|
||||
role="listitem"
|
||||
$gap="20px"
|
||||
$padding={{ vertical: '4xs', horizontal: isDesktop ? 'base' : 'xs' }}
|
||||
$css={css`
|
||||
@@ -65,7 +66,6 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
||||
>
|
||||
<Box
|
||||
$flex={flexLeft}
|
||||
role="gridcell"
|
||||
$css={css`
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
@@ -90,9 +90,15 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
||||
$align="center"
|
||||
$justify={isDesktop ? 'space-between' : 'flex-end'}
|
||||
$gap="32px"
|
||||
role="gridcell"
|
||||
>
|
||||
<StyledLink href={`/docs/${doc.id}`} tabIndex={-1}>
|
||||
<StyledLink
|
||||
href={`/docs/${doc.id}`}
|
||||
tabIndex={-1}
|
||||
aria-label={t('{{title}}, updated {{date}}', {
|
||||
title: doc.title || untitledDocument,
|
||||
date: dateToDisplay,
|
||||
})}
|
||||
>
|
||||
<DocsGridItemDate
|
||||
doc={doc}
|
||||
isDesktop={isDesktop}
|
||||
@@ -196,23 +202,11 @@ const IconPublic = ({ isPublic }: { isPublic: boolean }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DocsGridItemDate = ({
|
||||
doc,
|
||||
isDesktop,
|
||||
isInTrashbin,
|
||||
}: {
|
||||
doc: Doc;
|
||||
isDesktop: boolean;
|
||||
isInTrashbin: boolean;
|
||||
}) => {
|
||||
const useDateToDisplay = (doc: Doc, isInTrashbin: boolean) => {
|
||||
const { data: config } = useConfig();
|
||||
const { t } = useTranslation();
|
||||
const { relativeDate, calculateDaysLeft } = useDate();
|
||||
|
||||
if (!isDesktop) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let dateToDisplay = relativeDate(doc.updated_at);
|
||||
|
||||
if (isInTrashbin && config?.TRASHBIN_CUTOFF_DAYS && doc.deleted_at) {
|
||||
@@ -224,6 +218,24 @@ export const DocsGridItemDate = ({
|
||||
dateToDisplay = `${daysLeft} ${t('days', { count: daysLeft })}`;
|
||||
}
|
||||
|
||||
return dateToDisplay;
|
||||
};
|
||||
|
||||
export const DocsGridItemDate = ({
|
||||
doc,
|
||||
isDesktop,
|
||||
isInTrashbin,
|
||||
}: {
|
||||
doc: Doc;
|
||||
isDesktop: boolean;
|
||||
isInTrashbin: boolean;
|
||||
}) => {
|
||||
const dateToDisplay = useDateToDisplay(doc, isInTrashbin);
|
||||
|
||||
if (!isDesktop) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
$size="xs"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import i18next from 'i18next';
|
||||
import { DateTime } from 'luxon';
|
||||
@@ -73,7 +73,9 @@ describe('DocsGridItemDate', () => {
|
||||
});
|
||||
|
||||
it(`should render rendered the updated_at field in the correct language`, async () => {
|
||||
await i18next.changeLanguage('fr');
|
||||
await act(async () => {
|
||||
await i18next.changeLanguage('fr');
|
||||
});
|
||||
|
||||
render(
|
||||
<DocsGridItemDate
|
||||
@@ -90,7 +92,9 @@ describe('DocsGridItemDate', () => {
|
||||
|
||||
expect(screen.getByText('il y a 5 jours')).toBeInTheDocument();
|
||||
|
||||
await i18next.changeLanguage('en');
|
||||
await act(async () => {
|
||||
await i18next.changeLanguage('en');
|
||||
});
|
||||
});
|
||||
|
||||
[
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user