Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5eca3deefc | |||
| f5d10360ae | |||
| afa93e4cdd | |||
| f9c9e444c9 | |||
| e1d2d9e5c8 | |||
| ab92fc43d6 | |||
| 3a3ed0453b | |||
| 43a1a76a2f | |||
| 62213812ee | |||
| 3d2b018927 | |||
| bb0502b49b | |||
| 9893558c74 | |||
| ea3a4a6da3 | |||
| b78ad27a71 | |||
| e4b8ffb304 | |||
| 78c7ab247b | |||
| b0bd6e2c01 | |||
| 37527416f2 | |||
| 30bc959340 | |||
| a73d9c1c78 | |||
| a920daf05b | |||
| ff88465398 | |||
| 3617e4f7b8 | |||
| efaec45bfd | |||
| 715d88ba3c | |||
| 7d64d79eeb | |||
| 2e66b87dab | |||
| 970fcd53d6 | |||
| eeeaa0c76d | |||
| 9af753a28f | |||
| 75fe73c34b | |||
| aebe7d06d2 |
+28
-1
@@ -6,6 +6,32 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- ✨ Import of documents #7765
|
||||
- ✨(backend) add documents/all endpoint with descendants #1553
|
||||
- ✅(export) add PDF regression tests #1762
|
||||
|
||||
## [4.3.0] - 2026-01-05
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(helm) redirecting system #1697
|
||||
- 📱(frontend) add comments for smaller device #1737
|
||||
- ✨(project) add custom js support via config #1759
|
||||
|
||||
### Changed
|
||||
|
||||
- 🥅(frontend) intercept 401 error on GET threads #1754
|
||||
- 🦺(frontend) check content type pdf on PdfBlock #1756
|
||||
- ✈️(frontend) pause Posthog when offline #1755
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) fix tables deletion #1739
|
||||
- 🐛(frontend) fix children not display when first resize #1753
|
||||
- 🐛(frontend) fix clickable main content regression #1773
|
||||
|
||||
## [4.2.0] - 2025-12-17
|
||||
|
||||
### Added
|
||||
@@ -944,7 +970,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.2.0...main
|
||||
[unreleased]: https://github.com/suitenumerique/docs/compare/v4.3.0...main
|
||||
[v4.3.0]: https://github.com/suitenumerique/docs/releases/v4.3.0
|
||||
[v4.2.0]: https://github.com/suitenumerique/docs/releases/v4.2.0
|
||||
[v4.1.0]: https://github.com/suitenumerique/docs/releases/v4.1.0
|
||||
[v4.0.0]: https://github.com/suitenumerique/docs/releases/v4.0.0
|
||||
|
||||
@@ -213,6 +213,7 @@ logs: ## display app-dev logs (follow mode)
|
||||
.PHONY: logs
|
||||
|
||||
run-backend: ## Start only the backend application and all needed services
|
||||
@$(COMPOSE) up --force-recreate -d docspec
|
||||
@$(COMPOSE) up --force-recreate -d celery-dev
|
||||
@$(COMPOSE) up --force-recreate -d y-provider-development
|
||||
@$(COMPOSE) up --force-recreate -d nginx
|
||||
|
||||
@@ -231,6 +231,14 @@ services:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
|
||||
docspec:
|
||||
image: ghcr.io/docspecio/api:2.6.0
|
||||
ports:
|
||||
- "4000:4000"
|
||||
networks:
|
||||
- lasuite
|
||||
- default
|
||||
|
||||
networks:
|
||||
lasuite:
|
||||
name: lasuite-network
|
||||
|
||||
+14
-13
@@ -7,7 +7,7 @@ Here we describe all environment variables that can be set for the docs applicat
|
||||
These are the environment variables you can set for the `impress-backend` container.
|
||||
|
||||
| Option | Description | default |
|
||||
|-------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
|
||||
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
|
||||
| AI_API_KEY | AI key to be used for AI Base url | |
|
||||
| AI_BASE_URL | OpenAI compatible AI base url | |
|
||||
@@ -58,12 +58,14 @@ These are the environment variables you can set for the `impress-backend` contai
|
||||
| DJANGO_EMAIL_USE_TLS | Use tls for email host connection | false |
|
||||
| DJANGO_SECRET_KEY | Secret key | |
|
||||
| DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] |
|
||||
| DOCSPEC_API_URL | URL to endpoint of DocSpec conversion API | |
|
||||
| DOCUMENT_IMAGE_MAX_SIZE | Maximum size of document in bytes | 10485760 |
|
||||
| FRONTEND_CSS_URL | To add a external css file to the app | |
|
||||
| FRONTEND_JS_URL | To add a external js file to the app | |
|
||||
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | Frontend feature flag to display the homepage | false |
|
||||
| FRONTEND_THEME | Frontend theme to use | |
|
||||
| LANGUAGE_CODE | Default language | en-us |
|
||||
| LASUITE_MARKETING_BACKEND | Backend used when SIGNUP_NEW_USER_TO_MARKETING_EMAIL is True. See https://github.com/suitenumerique/django-lasuite/blob/main/documentation/how-to-use-marketing-backend.md | lasuite.marketing.backends.dummy.DummyBackend |
|
||||
| LASUITE_MARKETING_BACKEND | Backend used when SIGNUP_NEW_USER_TO_MARKETING_EMAIL is True. See https://github.com/suitenumerique/django-lasuite/blob/main/documentation/how-to-use-marketing-backend.md | lasuite.marketing.backends.dummy.DummyBackend |
|
||||
| LASUITE_MARKETING_PARAMETERS | The parameters to configure LASUITE_MARKETING_BACKEND. See https://github.com/suitenumerique/django-lasuite/blob/main/documentation/how-to-use-marketing-backend.md | {} |
|
||||
| LOGGING_LEVEL_LOGGERS_APP | Application logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
|
||||
| LOGGING_LEVEL_LOGGERS_ROOT | Default logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
|
||||
@@ -113,7 +115,6 @@ These are the environment variables you can set for the `impress-backend` contai
|
||||
| Y_PROVIDER_API_BASE_URL | Y Provider url | |
|
||||
| Y_PROVIDER_API_KEY | Y provider API key | |
|
||||
|
||||
|
||||
## impress-frontend image
|
||||
|
||||
These are the environment variables you can set to build the `impress-frontend` image.
|
||||
@@ -126,7 +127,7 @@ Example:
|
||||
|
||||
```
|
||||
docker build -f src/frontend/Dockerfile --target frontend-production --build-arg PUBLISH_AS_MIT=false docs-frontend:latest
|
||||
```
|
||||
```
|
||||
|
||||
If you want to build the front-end application using the yarn build command, you can edit the file `src/frontend/apps/impress/.env` with the `NODE_ENV=production` environment variable and modify it. Alternatively, you can use the listed environment variables with the prefix `NEXT_PUBLIC_` (for example, `NEXT_PUBLIC_PUBLISH_AS_MIT=false`).
|
||||
|
||||
@@ -137,18 +138,18 @@ cd src/frontend/apps/impress
|
||||
NODE_ENV=production NEXT_PUBLIC_PUBLISH_AS_MIT=false yarn build
|
||||
```
|
||||
|
||||
| Option | Description | default |
|
||||
| ----------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| API_ORIGIN | backend domain - it uses the current domain if not initialized | |
|
||||
| SW_DEACTIVATED | To not install the service worker | |
|
||||
| PUBLISH_AS_MIT | Removes packages whose licences are incompatible with the MIT licence (see below) | true |
|
||||
| Option | Description | default |
|
||||
| -------------- | --------------------------------------------------------------------------------- | ------- |
|
||||
| API_ORIGIN | backend domain - it uses the current domain if not initialized | |
|
||||
| SW_DEACTIVATED | To not install the service worker | |
|
||||
| PUBLISH_AS_MIT | Removes packages whose licences are incompatible with the MIT licence (see below) | true |
|
||||
|
||||
Packages with licences incompatible with the MIT licence:
|
||||
* `xl-docx-exporter`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-docx-exporter/LICENSE),
|
||||
* `xl-pdf-exporter`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE),
|
||||
* `xl-multi-column`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-multi-column/LICENSE).
|
||||
|
||||
- `xl-docx-exporter`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-docx-exporter/LICENSE),
|
||||
- `xl-pdf-exporter`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE),
|
||||
- `xl-multi-column`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-multi-column/LICENSE).
|
||||
|
||||
In `.env.development`, `PUBLISH_AS_MIT` is set to `false`, allowing developers to test Docs with all its features.
|
||||
|
||||
⚠️ If you run Docs in production with `PUBLISH_AS_MIT` set to `false` make sure you fulfill your BlockNote licensing or [subscription](https://www.blocknotejs.org/about#partner-with-us) obligations.
|
||||
|
||||
|
||||
+56
-1
@@ -8,7 +8,7 @@ To use this feature, simply set the `FRONTEND_CSS_URL` environment variable to t
|
||||
FRONTEND_CSS_URL=http://anything/custom-style.css
|
||||
```
|
||||
|
||||
Once you've set this variable, our application will load your custom CSS file and apply the styles to our frontend application.
|
||||
Once you've set this variable, Docs will load your custom CSS file and apply the styles to our frontend application.
|
||||
|
||||
### Benefits
|
||||
|
||||
@@ -32,6 +32,61 @@ Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom
|
||||
|
||||
----
|
||||
|
||||
# Runtime JavaScript Injection 🚀
|
||||
|
||||
### How to Use
|
||||
|
||||
To use this feature, simply set the `FRONTEND_JS_URL` environment variable to the URL of your custom JavaScript file. For example:
|
||||
|
||||
```javascript
|
||||
FRONTEND_JS_URL=http://anything/custom-script.js
|
||||
```
|
||||
|
||||
Once you've set this variable, Docs will load your custom JavaScript file and execute it in the browser, allowing you to modify the application's behavior at runtime.
|
||||
|
||||
### Benefits
|
||||
|
||||
This feature provides several benefits, including:
|
||||
|
||||
* **Dynamic customization** 🔄: With this feature, you can dynamically modify the behavior and appearance of our application without requiring any code changes.
|
||||
* **Flexibility** 🌈: You can add custom functionality, modify existing features, or integrate third-party services.
|
||||
* **Runtime injection** ⏱️: This feature allows you to inject JavaScript into the application at runtime, without requiring a restart or recompilation.
|
||||
|
||||
### Example Use Case
|
||||
|
||||
Let's say you want to add a custom menu to the application header. You can create a custom JavaScript file with the following contents:
|
||||
|
||||
```javascript
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
function initCustomMenu() {
|
||||
// Wait for the page to be fully loaded
|
||||
const header = document.querySelector('header');
|
||||
if (!header) return false;
|
||||
|
||||
// Create and inject your custom menu
|
||||
const customMenu = document.createElement('div');
|
||||
customMenu.innerHTML = '<button>Custom Menu</button>';
|
||||
header.appendChild(customMenu);
|
||||
|
||||
console.log('Custom menu added successfully');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initCustomMenu);
|
||||
} else {
|
||||
initCustomMenu();
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
Then, set the `FRONTEND_JS_URL` environment variable to the URL of your custom JavaScript file. Once you've done this, our application will load your custom JavaScript file and execute it, adding your custom menu to the header.
|
||||
|
||||
----
|
||||
|
||||
# **Your Docs icon** 📝
|
||||
|
||||
You can add your own Docs icon in the header from the theme customization file.
|
||||
|
||||
@@ -76,6 +76,8 @@ DJANGO_SERVER_TO_SERVER_API_TOKENS=server-api-token
|
||||
Y_PROVIDER_API_BASE_URL=http://y-provider-development:4444/api/
|
||||
Y_PROVIDER_API_KEY=yprovider-api-key
|
||||
|
||||
DOCSPEC_API_URL=http://docspec:4000/conversion
|
||||
|
||||
# Theme customization
|
||||
THEME_CUSTOMIZATION_CACHE_TIMEOUT=15
|
||||
|
||||
|
||||
@@ -6,4 +6,4 @@ Y_PROVIDER_API_BASE_URL=http://y-provider:4444/api/
|
||||
|
||||
# Throttle
|
||||
API_DOCUMENT_THROTTLE_RATE=1000/min
|
||||
API_CONFIG_THROTTLE_RATE=1000/min
|
||||
API_CONFIG_THROTTLE_RATE=1000/min
|
||||
|
||||
@@ -30,8 +30,11 @@
|
||||
"groupName": "ignored js dependencies",
|
||||
"matchManagers": ["npm"],
|
||||
"matchPackageNames": [
|
||||
"@next/eslint-plugin-next",
|
||||
"docx",
|
||||
"eslint-config-next",
|
||||
"fetch-mock",
|
||||
"next",
|
||||
"node",
|
||||
"node-fetch",
|
||||
"workbox-webpack-plugin"
|
||||
|
||||
@@ -15,10 +15,11 @@ import magic
|
||||
from rest_framework import serializers
|
||||
|
||||
from core import choices, enums, models, utils, validators
|
||||
from core.services import mime_types
|
||||
from core.services.ai_services import AI_ACTIONS
|
||||
from core.services.converter_services import (
|
||||
ConversionError,
|
||||
YdocConverter,
|
||||
Converter,
|
||||
)
|
||||
|
||||
|
||||
@@ -188,6 +189,7 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
|
||||
content = serializers.CharField(required=False)
|
||||
websocket = serializers.BooleanField(required=False, write_only=True)
|
||||
file = serializers.FileField(required=False, write_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Document
|
||||
@@ -204,6 +206,7 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
"deleted_at",
|
||||
"depth",
|
||||
"excerpt",
|
||||
"file",
|
||||
"is_favorite",
|
||||
"link_role",
|
||||
"link_reach",
|
||||
@@ -461,7 +464,9 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
|
||||
language = user.language or language
|
||||
|
||||
try:
|
||||
document_content = YdocConverter().convert(validated_data["content"])
|
||||
document_content = Converter().convert(
|
||||
validated_data["content"], mime_types.MARKDOWN, mime_types.YJS
|
||||
)
|
||||
except ConversionError as err:
|
||||
raise serializers.ValidationError(
|
||||
{"content": ["Could not convert content"]}
|
||||
|
||||
@@ -41,17 +41,19 @@ from rest_framework.permissions import AllowAny
|
||||
|
||||
from core import authentication, choices, enums, models
|
||||
from core.api.filters import remove_accents
|
||||
from core.services import mime_types
|
||||
from core.services.ai_services import AIService
|
||||
from core.services.collaboration_services import CollaborationService
|
||||
from core.services.converter_services import (
|
||||
ConversionError,
|
||||
Converter,
|
||||
)
|
||||
from core.services.converter_services import (
|
||||
ServiceUnavailableError as YProviderServiceUnavailableError,
|
||||
)
|
||||
from core.services.converter_services import (
|
||||
ValidationError as YProviderValidationError,
|
||||
)
|
||||
from core.services.converter_services import (
|
||||
YdocConverter,
|
||||
)
|
||||
from core.services.search_indexers import (
|
||||
get_document_indexer,
|
||||
get_visited_document_ids_of,
|
||||
@@ -388,6 +390,7 @@ class DocumentViewSet(
|
||||
queryset = models.Document.objects.select_related("creator").all()
|
||||
serializer_class = serializers.DocumentSerializer
|
||||
ai_translate_serializer_class = serializers.AITranslateSerializer
|
||||
all_serializer_class = serializers.ListDocumentSerializer
|
||||
children_serializer_class = serializers.ListDocumentSerializer
|
||||
descendants_serializer_class = serializers.ListDocumentSerializer
|
||||
list_serializer_class = serializers.ListDocumentSerializer
|
||||
@@ -524,6 +527,28 @@ class DocumentViewSet(
|
||||
"IN SHARE ROW EXCLUSIVE MODE;"
|
||||
)
|
||||
|
||||
# Remove file from validated_data as it's not a model field
|
||||
# Process it if present
|
||||
uploaded_file = serializer.validated_data.pop("file", None)
|
||||
|
||||
# If a file is uploaded, convert it to Yjs format and set as content
|
||||
if uploaded_file:
|
||||
try:
|
||||
file_content = uploaded_file.read()
|
||||
|
||||
converter = Converter()
|
||||
converted_content = converter.convert(
|
||||
file_content,
|
||||
content_type=uploaded_file.content_type,
|
||||
accept=mime_types.YJS,
|
||||
)
|
||||
serializer.validated_data["content"] = converted_content
|
||||
serializer.validated_data["title"] = uploaded_file.name
|
||||
except ConversionError as err:
|
||||
raise drf.exceptions.ValidationError(
|
||||
{"file": ["Could not convert file content"]}
|
||||
) from err
|
||||
|
||||
obj = models.Document.add_root(
|
||||
creator=self.request.user,
|
||||
**serializer.validated_data,
|
||||
@@ -858,6 +883,60 @@ class DocumentViewSet(
|
||||
},
|
||||
)
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=False,
|
||||
methods=["get"],
|
||||
)
|
||||
def all(self, request, *args, **kwargs):
|
||||
"""
|
||||
Returns all documents (including descendants) that the user has access to.
|
||||
|
||||
Unlike the list endpoint which only returns top-level documents, this endpoint
|
||||
returns all documents including children, grandchildren, etc.
|
||||
"""
|
||||
user = self.request.user
|
||||
|
||||
accessible_documents = self.get_queryset()
|
||||
accessible_paths = list(accessible_documents.values_list("path", flat=True))
|
||||
|
||||
if not accessible_paths:
|
||||
return self.get_response_for_queryset(self.queryset.none())
|
||||
|
||||
# Build query to include all descendants using path prefix matching
|
||||
descendants_clause = db.Q()
|
||||
for path in accessible_paths:
|
||||
descendants_clause |= db.Q(path__startswith=path)
|
||||
|
||||
queryset = self.queryset.filter(
|
||||
descendants_clause, ancestors_deleted_at__isnull=True
|
||||
)
|
||||
|
||||
# Apply existing filters
|
||||
filterset = ListDocumentFilter(
|
||||
self.request.GET, queryset=queryset, request=self.request
|
||||
)
|
||||
if not filterset.is_valid():
|
||||
raise drf.exceptions.ValidationError(filterset.errors)
|
||||
filter_data = filterset.form.cleaned_data
|
||||
|
||||
# Filter as early as possible on fields that are available on the model
|
||||
for field in ["is_creator_me", "title"]:
|
||||
queryset = filterset.filters[field].filter(queryset, filter_data[field])
|
||||
|
||||
queryset = queryset.annotate_user_roles(user)
|
||||
|
||||
# Annotate favorite status and filter if applicable as late as possible
|
||||
queryset = queryset.annotate_is_favorite(user)
|
||||
for field in ["is_favorite", "is_masked"]:
|
||||
queryset = filterset.filters[field].filter(queryset, filter_data[field])
|
||||
|
||||
# Apply ordering only now that everything is filtered and annotated
|
||||
queryset = filters.OrderingFilter().filter_queryset(
|
||||
self.request, queryset, self
|
||||
)
|
||||
|
||||
return self.get_response_for_queryset(queryset)
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=True,
|
||||
methods=["get"],
|
||||
@@ -1700,14 +1779,14 @@ class DocumentViewSet(
|
||||
if base64_content is not None:
|
||||
# Convert using the y-provider service
|
||||
try:
|
||||
yprovider = YdocConverter()
|
||||
yprovider = Converter()
|
||||
result = yprovider.convert(
|
||||
base64.b64decode(base64_content),
|
||||
"application/vnd.yjs.doc",
|
||||
mime_types.YJS,
|
||||
{
|
||||
"markdown": "text/markdown",
|
||||
"html": "text/html",
|
||||
"json": "application/json",
|
||||
"markdown": mime_types.MARKDOWN,
|
||||
"html": mime_types.HTML,
|
||||
"json": mime_types.JSON,
|
||||
}[content_format],
|
||||
)
|
||||
content = result
|
||||
@@ -2197,6 +2276,7 @@ class ConfigView(drf.views.APIView):
|
||||
"ENVIRONMENT",
|
||||
"FRONTEND_CSS_URL",
|
||||
"FRONTEND_HOMEPAGE_FEATURE_ENABLED",
|
||||
"FRONTEND_JS_URL",
|
||||
"FRONTEND_THEME",
|
||||
"MEDIA_BASE_URL",
|
||||
"POSTHOG_KEY",
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""Y-Provider API services."""
|
||||
|
||||
import typing
|
||||
from base64 import b64encode
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
import requests
|
||||
|
||||
from core.services import mime_types
|
||||
|
||||
|
||||
class ConversionError(Exception):
|
||||
"""Base exception for conversion-related errors."""
|
||||
@@ -19,8 +22,73 @@ class ServiceUnavailableError(ConversionError):
|
||||
"""Raised when the conversion service is unavailable."""
|
||||
|
||||
|
||||
class ConverterProtocol(typing.Protocol):
|
||||
"""Protocol for converter classes."""
|
||||
|
||||
def convert(self, data, content_type, accept):
|
||||
"""Convert content from one format to another."""
|
||||
|
||||
|
||||
class Converter:
|
||||
"""Orchestrates conversion between different formats using specialized converters."""
|
||||
|
||||
docspec: ConverterProtocol
|
||||
ydoc: ConverterProtocol
|
||||
|
||||
def __init__(self):
|
||||
self.docspec = DocSpecConverter()
|
||||
self.ydoc = YdocConverter()
|
||||
|
||||
def convert(self, data, content_type, accept):
|
||||
"""Convert input into other formats using external microservices."""
|
||||
|
||||
if content_type == mime_types.DOCX and accept == mime_types.YJS:
|
||||
blocknote_data = self.docspec.convert(
|
||||
data, mime_types.DOCX, mime_types.BLOCKNOTE
|
||||
)
|
||||
return self.ydoc.convert(
|
||||
blocknote_data, mime_types.BLOCKNOTE, mime_types.YJS
|
||||
)
|
||||
|
||||
return self.ydoc.convert(data, content_type, accept)
|
||||
|
||||
|
||||
class DocSpecConverter:
|
||||
"""Service class for DocSpec conversion-related operations."""
|
||||
|
||||
def _request(self, url, data, content_type):
|
||||
"""Make a request to the DocSpec API."""
|
||||
|
||||
response = requests.post(
|
||||
url,
|
||||
headers={"Accept": mime_types.BLOCKNOTE},
|
||||
files={"file": ("document.docx", data, content_type)},
|
||||
timeout=settings.CONVERSION_API_TIMEOUT,
|
||||
verify=settings.CONVERSION_API_SECURE,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
def convert(self, data, content_type, accept):
|
||||
"""Convert a Document to BlockNote."""
|
||||
if not data:
|
||||
raise ValidationError("Input data cannot be empty")
|
||||
|
||||
if content_type != mime_types.DOCX or accept != mime_types.BLOCKNOTE:
|
||||
raise ValidationError(
|
||||
f"Conversion from {content_type} to {accept} is not supported."
|
||||
)
|
||||
|
||||
try:
|
||||
return self._request(settings.DOCSPEC_API_URL, data, content_type).content
|
||||
except requests.RequestException as err:
|
||||
raise ServiceUnavailableError(
|
||||
"Failed to connect to DocSpec conversion service",
|
||||
) from err
|
||||
|
||||
|
||||
class YdocConverter:
|
||||
"""Service class for conversion-related operations."""
|
||||
"""Service class for YDoc conversion-related operations."""
|
||||
|
||||
@property
|
||||
def auth_header(self):
|
||||
@@ -44,29 +112,27 @@ class YdocConverter:
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
def convert(
|
||||
self, text, content_type="text/markdown", accept="application/vnd.yjs.doc"
|
||||
):
|
||||
def convert(self, data, content_type=mime_types.MARKDOWN, accept=mime_types.YJS):
|
||||
"""Convert a Markdown text into our internal format using an external microservice."""
|
||||
|
||||
if not text:
|
||||
raise ValidationError("Input text cannot be empty")
|
||||
if not data:
|
||||
raise ValidationError("Input data cannot be empty")
|
||||
|
||||
try:
|
||||
response = self._request(
|
||||
f"{settings.Y_PROVIDER_API_BASE_URL}{settings.CONVERSION_API_ENDPOINT}/",
|
||||
text,
|
||||
data,
|
||||
content_type,
|
||||
accept,
|
||||
)
|
||||
if accept == "application/vnd.yjs.doc":
|
||||
if accept == mime_types.YJS:
|
||||
return b64encode(response.content).decode("utf-8")
|
||||
if accept in {"text/markdown", "text/html"}:
|
||||
if accept in {mime_types.MARKDOWN, "text/html"}:
|
||||
return response.text
|
||||
if accept == "application/json":
|
||||
if accept == mime_types.JSON:
|
||||
return response.json()
|
||||
raise ValidationError("Unsupported format")
|
||||
except requests.RequestException as err:
|
||||
raise ServiceUnavailableError(
|
||||
"Failed to connect to conversion service",
|
||||
f"Failed to connect to YDoc conversion service {content_type}, {accept}",
|
||||
) from err
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
"""MIME type constants for document conversion."""
|
||||
|
||||
BLOCKNOTE = "application/vnd.blocknote+json"
|
||||
YJS = "application/vnd.yjs.doc"
|
||||
MARKDOWN = "text/markdown"
|
||||
JSON = "application/json"
|
||||
DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
HTML = "text/html"
|
||||
@@ -0,0 +1,427 @@
|
||||
"""
|
||||
Tests for Documents API endpoint in impress's core app: all
|
||||
|
||||
The 'all' endpoint returns ALL documents (including descendants) that the user has access to.
|
||||
This is different from the 'list' endpoint which only returns top-level documents.
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest import mock
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", models.LinkRoleChoices.values)
|
||||
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
|
||||
def test_api_documents_all_anonymous(reach, role):
|
||||
"""
|
||||
Anonymous users should not be able to list any documents via the all endpoint
|
||||
whatever the link reach and link role.
|
||||
"""
|
||||
parent = factories.DocumentFactory(link_reach=reach, link_role=role)
|
||||
factories.DocumentFactory(parent=parent, link_reach=reach, link_role=role)
|
||||
|
||||
response = APIClient().get("/api/v1.0/documents/all/")
|
||||
|
||||
assert response.status_code == 200
|
||||
results = response.json()["results"]
|
||||
assert len(results) == 0
|
||||
|
||||
|
||||
def test_api_documents_all_authenticated_with_children():
|
||||
"""
|
||||
Authenticated users should see all documents including children,
|
||||
even though children don't have DocumentAccess records.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Create a document tree: parent -> child -> grandchild
|
||||
parent = factories.DocumentFactory()
|
||||
factories.UserDocumentAccessFactory(document=parent, user=user, role="owner")
|
||||
|
||||
child = factories.DocumentFactory(parent=parent)
|
||||
grandchild = factories.DocumentFactory(parent=child)
|
||||
|
||||
# Verify setup
|
||||
assert models.DocumentAccess.objects.filter(document=parent).count() == 1
|
||||
assert models.DocumentAccess.objects.filter(document=child).count() == 0
|
||||
assert models.DocumentAccess.objects.filter(document=grandchild).count() == 0
|
||||
|
||||
response = client.get("/api/v1.0/documents/all/")
|
||||
|
||||
assert response.status_code == 200
|
||||
results = response.json()["results"]
|
||||
|
||||
# All three documents should be returned (parent + child + grandchild)
|
||||
assert len(results) == 3
|
||||
results_ids = {result["id"] for result in results}
|
||||
assert results_ids == {str(parent.id), str(child.id), str(grandchild.id)}
|
||||
|
||||
depths = {result["depth"] for result in results}
|
||||
assert depths == {1, 2, 3}
|
||||
|
||||
|
||||
def test_api_documents_all_authenticated_multiple_trees():
|
||||
"""
|
||||
Users should see all accessible documents from multiple document trees.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Tree 1: User has access
|
||||
tree1_parent = factories.DocumentFactory()
|
||||
factories.UserDocumentAccessFactory(document=tree1_parent, user=user)
|
||||
tree1_child = factories.DocumentFactory(parent=tree1_parent)
|
||||
|
||||
# Tree 2: User has access
|
||||
tree2_parent = factories.DocumentFactory()
|
||||
factories.UserDocumentAccessFactory(document=tree2_parent, user=user)
|
||||
tree2_child1 = factories.DocumentFactory(parent=tree2_parent)
|
||||
tree2_child2 = factories.DocumentFactory(parent=tree2_parent)
|
||||
|
||||
# Tree 3: User does NOT have access
|
||||
tree3_parent = factories.DocumentFactory()
|
||||
factories.DocumentFactory(parent=tree3_parent)
|
||||
|
||||
response = client.get("/api/v1.0/documents/all/")
|
||||
|
||||
assert response.status_code == 200
|
||||
results = response.json()["results"]
|
||||
|
||||
# Should return 5 documents (tree1: 2, tree2: 3, tree3: 0)
|
||||
assert len(results) == 5
|
||||
results_ids = {result["id"] for result in results}
|
||||
expected_ids = {
|
||||
str(tree1_parent.id),
|
||||
str(tree1_child.id),
|
||||
str(tree2_parent.id),
|
||||
str(tree2_child1.id),
|
||||
str(tree2_child2.id),
|
||||
}
|
||||
assert results_ids == expected_ids
|
||||
|
||||
|
||||
def test_api_documents_all_authenticated_explicit_access_to_parent_and_child():
|
||||
"""
|
||||
When a user has explicit DocumentAccess to both parent AND child,
|
||||
both should appear in the 'all' endpoint results (unlike 'list' which deduplicates).
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Parent with explicit access
|
||||
parent = factories.DocumentFactory()
|
||||
factories.UserDocumentAccessFactory(document=parent, user=user)
|
||||
|
||||
# Child also has explicit access (e.g., shared separately)
|
||||
child = factories.DocumentFactory(parent=parent)
|
||||
factories.UserDocumentAccessFactory(document=child, user=user)
|
||||
|
||||
# Grandchild has no explicit access
|
||||
grandchild = factories.DocumentFactory(parent=child)
|
||||
|
||||
# Verify setup
|
||||
assert models.DocumentAccess.objects.filter(document=parent).count() == 1
|
||||
assert models.DocumentAccess.objects.filter(document=child).count() == 1
|
||||
assert models.DocumentAccess.objects.filter(document=grandchild).count() == 0
|
||||
|
||||
response = client.get("/api/v1.0/documents/all/")
|
||||
|
||||
assert response.status_code == 200
|
||||
results = response.json()["results"]
|
||||
|
||||
# All three should appear
|
||||
assert len(results) == 3
|
||||
results_ids = {result["id"] for result in results}
|
||||
assert results_ids == {str(parent.id), str(child.id), str(grandchild.id)}
|
||||
|
||||
# Each document should appear exactly once (no duplicates)
|
||||
results_ids_list = [result["id"] for result in results]
|
||||
assert len(results_ids_list) == len(set(results_ids_list)) # No duplicates
|
||||
|
||||
|
||||
def test_api_documents_all_authenticated_via_team(mock_user_teams):
|
||||
"""
|
||||
Users should see all documents (including descendants) for documents accessed via teams.
|
||||
"""
|
||||
mock_user_teams.return_value = ["team1", "team2"]
|
||||
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Document tree via team1
|
||||
parent1 = factories.DocumentFactory()
|
||||
factories.TeamDocumentAccessFactory(document=parent1, team="team1")
|
||||
child1 = factories.DocumentFactory(parent=parent1)
|
||||
|
||||
# Document tree via team2
|
||||
parent2 = factories.DocumentFactory()
|
||||
factories.TeamDocumentAccessFactory(document=parent2, team="team2")
|
||||
child2 = factories.DocumentFactory(parent=parent2)
|
||||
|
||||
# Document tree via unknown team
|
||||
parent3 = factories.DocumentFactory()
|
||||
factories.TeamDocumentAccessFactory(document=parent3, team="team3")
|
||||
factories.DocumentFactory(parent=parent3)
|
||||
|
||||
response = client.get("/api/v1.0/documents/all/")
|
||||
|
||||
assert response.status_code == 200
|
||||
results = response.json()["results"]
|
||||
|
||||
# Should return 4 documents (team1: 2, team2: 2, team3: 0)
|
||||
assert len(results) == 4
|
||||
results_ids = {result["id"] for result in results}
|
||||
expected_ids = {
|
||||
str(parent1.id),
|
||||
str(child1.id),
|
||||
str(parent2.id),
|
||||
str(child2.id),
|
||||
}
|
||||
assert results_ids == expected_ids
|
||||
|
||||
|
||||
def test_api_documents_all_authenticated_soft_deleted():
|
||||
"""
|
||||
Soft-deleted documents and their descendants should not be included.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Active tree
|
||||
active_parent = factories.DocumentFactory()
|
||||
factories.UserDocumentAccessFactory(document=active_parent, user=user)
|
||||
active_child = factories.DocumentFactory(parent=active_parent)
|
||||
|
||||
# Soft-deleted tree
|
||||
deleted_parent = factories.DocumentFactory()
|
||||
factories.UserDocumentAccessFactory(document=deleted_parent, user=user)
|
||||
_deleted_child = factories.DocumentFactory(parent=deleted_parent)
|
||||
deleted_parent.soft_delete()
|
||||
|
||||
response = client.get("/api/v1.0/documents/all/")
|
||||
|
||||
assert response.status_code == 200
|
||||
results = response.json()["results"]
|
||||
|
||||
# Should only return active documents
|
||||
assert len(results) == 2
|
||||
results_ids = {result["id"] for result in results}
|
||||
assert results_ids == {str(active_parent.id), str(active_child.id)}
|
||||
|
||||
|
||||
def test_api_documents_all_authenticated_permanently_deleted():
|
||||
"""
|
||||
Permanently deleted documents should not be included.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Active tree
|
||||
active_parent = factories.DocumentFactory()
|
||||
factories.UserDocumentAccessFactory(document=active_parent, user=user)
|
||||
active_child = factories.DocumentFactory(parent=active_parent)
|
||||
|
||||
# Permanently deleted tree (deleted > 30 days ago)
|
||||
deleted_parent = factories.DocumentFactory()
|
||||
factories.UserDocumentAccessFactory(document=deleted_parent, user=user)
|
||||
_deleted_child = factories.DocumentFactory(parent=deleted_parent)
|
||||
|
||||
fourty_days_ago = timezone.now() - timedelta(days=40)
|
||||
with mock.patch("django.utils.timezone.now", return_value=fourty_days_ago):
|
||||
deleted_parent.soft_delete()
|
||||
|
||||
response = client.get("/api/v1.0/documents/all/")
|
||||
|
||||
assert response.status_code == 200
|
||||
results = response.json()["results"]
|
||||
|
||||
# Should only return active documents
|
||||
assert len(results) == 2
|
||||
results_ids = {result["id"] for result in results}
|
||||
assert results_ids == {str(active_parent.id), str(active_child.id)}
|
||||
|
||||
|
||||
def test_api_documents_all_authenticated_link_reach_restricted():
|
||||
"""
|
||||
Documents with link_reach=restricted accessed via LinkTrace should not appear
|
||||
in the all endpoint results.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Document with direct access (should appear)
|
||||
parent_with_access = factories.DocumentFactory()
|
||||
factories.UserDocumentAccessFactory(document=parent_with_access, user=user)
|
||||
child_with_access = factories.DocumentFactory(parent=parent_with_access)
|
||||
|
||||
# Document with only LinkTrace and restricted reach (should NOT appear)
|
||||
parent_restricted = factories.DocumentFactory(
|
||||
link_reach="restricted", link_traces=[user]
|
||||
)
|
||||
factories.DocumentFactory(parent=parent_restricted)
|
||||
|
||||
response = client.get("/api/v1.0/documents/all/")
|
||||
|
||||
assert response.status_code == 200
|
||||
results = response.json()["results"]
|
||||
|
||||
# Only documents with direct access should appear
|
||||
assert len(results) == 2
|
||||
results_ids = {result["id"] for result in results}
|
||||
assert results_ids == {str(parent_with_access.id), str(child_with_access.id)}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reach", ["public", "authenticated"])
|
||||
def test_api_documents_all_authenticated_link_reach_public_or_authenticated(reach):
|
||||
"""
|
||||
Documents with link_reach=public or authenticated accessed via LinkTrace
|
||||
should appear with all their descendants.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Document accessed via LinkTrace with non-restricted reach
|
||||
parent = factories.DocumentFactory(link_reach=reach, link_traces=[user])
|
||||
child = factories.DocumentFactory(parent=parent)
|
||||
grandchild = factories.DocumentFactory(parent=child)
|
||||
|
||||
response = client.get("/api/v1.0/documents/all/")
|
||||
|
||||
assert response.status_code == 200
|
||||
results = response.json()["results"]
|
||||
|
||||
# All descendants should be included
|
||||
assert len(results) == 3
|
||||
results_ids = {result["id"] for result in results}
|
||||
assert results_ids == {str(parent.id), str(child.id), str(grandchild.id)}
|
||||
|
||||
|
||||
def test_api_documents_all_format():
|
||||
"""Validate the format of documents as returned by the all endpoint."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
access = factories.UserDocumentAccessFactory(document=document, user=user)
|
||||
child = factories.DocumentFactory(parent=document)
|
||||
|
||||
response = client.get("/api/v1.0/documents/all/")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
results = content.pop("results")
|
||||
|
||||
# Check pagination structure
|
||||
assert content == {
|
||||
"count": 2,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
}
|
||||
|
||||
# Verify parent document format
|
||||
parent_result = [r for r in results if r["id"] == str(document.id)][0]
|
||||
assert parent_result == {
|
||||
"id": str(document.id),
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 1,
|
||||
"excerpt": document.excerpt,
|
||||
"is_favorite": False,
|
||||
"link_reach": document.link_reach,
|
||||
"link_role": document.link_role,
|
||||
"nb_accesses_ancestors": 1,
|
||||
"nb_accesses_direct": 1,
|
||||
"numchild": 1,
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_role": access.role,
|
||||
}
|
||||
|
||||
# Verify child document format
|
||||
child_result = [r for r in results if r["id"] == str(child.id)][0]
|
||||
assert child_result["depth"] == 2
|
||||
assert child_result["user_role"] == access.role # Inherited from parent
|
||||
assert child_result["nb_accesses_direct"] == 0 # No direct access on child
|
||||
|
||||
|
||||
def test_api_documents_all_distinct():
|
||||
"""
|
||||
A document should only appear once even if the user has multiple access paths to it.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
other_user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Document with multiple accesses for the same user
|
||||
document = factories.DocumentFactory(users=[user, other_user])
|
||||
child = factories.DocumentFactory(parent=document)
|
||||
|
||||
response = client.get("/api/v1.0/documents/all/")
|
||||
|
||||
assert response.status_code == 200
|
||||
results = response.json()["results"]
|
||||
|
||||
# Should return 2 documents (parent + child), each appearing once
|
||||
assert len(results) == 2
|
||||
results_ids = [result["id"] for result in results]
|
||||
assert results_ids.count(str(document.id)) == 1
|
||||
assert results_ids.count(str(child.id)) == 1
|
||||
|
||||
|
||||
def test_api_documents_all_comparison_with_list():
|
||||
"""
|
||||
The 'all' endpoint should return more documents than 'list' when there are children.
|
||||
'list' returns only top-level documents, 'all' returns all descendants.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Create a document tree
|
||||
parent = factories.DocumentFactory()
|
||||
factories.UserDocumentAccessFactory(document=parent, user=user)
|
||||
child = factories.DocumentFactory(parent=parent)
|
||||
grandchild = factories.DocumentFactory(parent=child)
|
||||
|
||||
# Call list endpoint
|
||||
list_response = client.get("/api/v1.0/documents/")
|
||||
list_results = list_response.json()["results"]
|
||||
|
||||
# Call all endpoint
|
||||
all_response = client.get("/api/v1.0/documents/all/")
|
||||
all_results = all_response.json()["results"]
|
||||
|
||||
# list should return only parent
|
||||
assert len(list_results) == 1
|
||||
assert list_results[0]["id"] == str(parent.id)
|
||||
|
||||
# all should return parent + child + grandchild
|
||||
assert len(all_results) == 3
|
||||
all_ids = {result["id"] for result in all_results}
|
||||
assert all_ids == {str(parent.id), str(child.id), str(grandchild.id)}
|
||||
@@ -16,6 +16,7 @@ from rest_framework.test import APIClient
|
||||
from core import factories
|
||||
from core.api.serializers import ServerCreateDocumentSerializer
|
||||
from core.models import Document, Invitation, User
|
||||
from core.services import mime_types
|
||||
from core.services.converter_services import ConversionError, YdocConverter
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
@@ -191,7 +192,9 @@ def test_api_documents_create_for_owner_existing(mock_convert_md):
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
mock_convert_md.assert_called_once_with(
|
||||
"Document content", mime_types.MARKDOWN, mime_types.YJS
|
||||
)
|
||||
|
||||
document = Document.objects.get()
|
||||
assert response.json() == {"id": str(document.id)}
|
||||
@@ -236,7 +239,9 @@ def test_api_documents_create_for_owner_new_user(mock_convert_md):
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
mock_convert_md.assert_called_once_with(
|
||||
"Document content", mime_types.MARKDOWN, mime_types.YJS
|
||||
)
|
||||
|
||||
document = Document.objects.get()
|
||||
assert response.json() == {"id": str(document.id)}
|
||||
@@ -297,7 +302,9 @@ def test_api_documents_create_for_owner_existing_user_email_no_sub_with_fallback
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
mock_convert_md.assert_called_once_with(
|
||||
"Document content", mime_types.MARKDOWN, mime_types.YJS
|
||||
)
|
||||
|
||||
document = Document.objects.get()
|
||||
assert response.json() == {"id": str(document.id)}
|
||||
@@ -393,7 +400,9 @@ def test_api_documents_create_for_owner_new_user_no_sub_no_fallback_allow_duplic
|
||||
HTTP_AUTHORIZATION="Bearer DummyToken",
|
||||
)
|
||||
assert response.status_code == 201
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
mock_convert_md.assert_called_once_with(
|
||||
"Document content", mime_types.MARKDOWN, mime_types.YJS
|
||||
)
|
||||
|
||||
document = Document.objects.get()
|
||||
assert response.json() == {"id": str(document.id)}
|
||||
@@ -474,7 +483,9 @@ def test_api_documents_create_for_owner_with_default_language(
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
mock_convert_md.assert_called_once_with(
|
||||
"Document content", mime_types.MARKDOWN, mime_types.YJS
|
||||
)
|
||||
assert mock_send.call_args[0][3] == "de-de"
|
||||
|
||||
|
||||
@@ -501,7 +512,9 @@ def test_api_documents_create_for_owner_with_custom_language(mock_convert_md):
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
mock_convert_md.assert_called_once_with(
|
||||
"Document content", mime_types.MARKDOWN, mime_types.YJS
|
||||
)
|
||||
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
@@ -537,7 +550,9 @@ def test_api_documents_create_for_owner_with_custom_subject_and_message(
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
mock_convert_md.assert_called_once_with(
|
||||
"Document content", mime_types.MARKDOWN, mime_types.YJS
|
||||
)
|
||||
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
@@ -571,7 +586,9 @@ def test_api_documents_create_for_owner_with_converter_exception(
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Bearer DummyToken",
|
||||
)
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
mock_convert_md.assert_called_once_with(
|
||||
"Document content", mime_types.MARKDOWN, mime_types.YJS
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"content": ["Could not convert content"]}
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
"""
|
||||
Tests for Documents API endpoint in impress's core app: create with file upload
|
||||
"""
|
||||
|
||||
from base64 import b64decode, binascii
|
||||
from io import BytesIO
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
from core.models import Document
|
||||
from core.services import mime_types
|
||||
from core.services.converter_services import (
|
||||
ConversionError,
|
||||
ServiceUnavailableError,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_documents_create_with_file_anonymous():
|
||||
"""Anonymous users should not be allowed to create documents with file upload."""
|
||||
# Create a fake DOCX file
|
||||
file_content = b"fake docx content"
|
||||
file = BytesIO(file_content)
|
||||
file.name = "test_document.docx"
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/",
|
||||
{
|
||||
"file": file,
|
||||
},
|
||||
format="multipart",
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
assert not Document.objects.exists()
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_docx_file_success(mock_convert):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Mock the conversion
|
||||
converted_yjs = "base64encodedyjscontent"
|
||||
mock_convert.return_value = converted_yjs
|
||||
|
||||
# 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 == 201
|
||||
document = Document.objects.get()
|
||||
assert document.title == "My Important Document.docx"
|
||||
assert document.content == converted_yjs
|
||||
assert document.accesses.filter(role="owner", user=user).exists()
|
||||
|
||||
# Verify the converter was called correctly
|
||||
mock_convert.assert_called_once_with(
|
||||
file_content,
|
||||
content_type=mime_types.DOCX,
|
||||
accept=mime_types.YJS,
|
||||
)
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_markdown_file_success(mock_convert):
|
||||
"""
|
||||
Authenticated users should be able to create documents by uploading a Markdown file.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Mock the conversion
|
||||
converted_yjs = "base64encodedyjscontent"
|
||||
mock_convert.return_value = converted_yjs
|
||||
|
||||
# Create a fake Markdown file
|
||||
file_content = b"# Test Document\n\nThis is a test."
|
||||
file = BytesIO(file_content)
|
||||
file.name = "readme.md"
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/documents/",
|
||||
{
|
||||
"file": file,
|
||||
},
|
||||
format="multipart",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
document = Document.objects.get()
|
||||
assert document.title == "readme.md"
|
||||
assert document.content == converted_yjs
|
||||
assert document.accesses.filter(role="owner", user=user).exists()
|
||||
|
||||
# Verify the converter was called correctly
|
||||
mock_convert.assert_called_once_with(
|
||||
file_content,
|
||||
content_type=mime_types.MARKDOWN,
|
||||
accept=mime_types.YJS,
|
||||
)
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_file_and_explicit_title(mock_convert):
|
||||
"""
|
||||
When both file and title are provided, the filename should override the title.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Mock the conversion
|
||||
converted_yjs = "base64encodedyjscontent"
|
||||
mock_convert.return_value = converted_yjs
|
||||
|
||||
# Create a fake DOCX file
|
||||
file_content = b"fake docx content"
|
||||
file = BytesIO(file_content)
|
||||
file.name = "Uploaded Document.docx"
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/documents/",
|
||||
{
|
||||
"file": file,
|
||||
"title": "This should be overridden",
|
||||
},
|
||||
format="multipart",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
document = Document.objects.get()
|
||||
# The filename should take precedence
|
||||
assert document.title == "Uploaded Document.docx"
|
||||
|
||||
|
||||
def test_api_documents_create_with_empty_file():
|
||||
"""
|
||||
Creating a document with an empty file should fail with a validation error.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Create an empty file
|
||||
file = BytesIO(b"")
|
||||
file.name = "empty.docx"
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/documents/",
|
||||
{
|
||||
"file": file,
|
||||
},
|
||||
format="multipart",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"file": ["The submitted file is empty."]}
|
||||
assert not Document.objects.exists()
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_file_conversion_error(mock_convert):
|
||||
"""
|
||||
When conversion fails, the API should return a 400 error with appropriate message.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Mock the conversion to raise an error
|
||||
mock_convert.side_effect = ConversionError("Failed to convert document")
|
||||
|
||||
# Create a fake DOCX file
|
||||
file_content = b"fake invalid docx content"
|
||||
file = BytesIO(file_content)
|
||||
file.name = "corrupted.docx"
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/documents/",
|
||||
{
|
||||
"file": file,
|
||||
},
|
||||
format="multipart",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"file": ["Could not convert file content"]}
|
||||
assert not Document.objects.exists()
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_file_service_unavailable(mock_convert):
|
||||
"""
|
||||
When the conversion service is unavailable, appropriate error should be returned.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Mock the conversion to raise ServiceUnavailableError
|
||||
mock_convert.side_effect = ServiceUnavailableError(
|
||||
"Failed to connect to conversion service"
|
||||
)
|
||||
|
||||
# Create a fake DOCX file
|
||||
file_content = b"fake docx content"
|
||||
file = BytesIO(file_content)
|
||||
file.name = "document.docx"
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/documents/",
|
||||
{
|
||||
"file": file,
|
||||
},
|
||||
format="multipart",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"file": ["Could not convert file content"]}
|
||||
assert not Document.objects.exists()
|
||||
|
||||
|
||||
def test_api_documents_create_without_file_still_works():
|
||||
"""
|
||||
Creating a document without a file should still work as before (backward compatibility).
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/documents/",
|
||||
{
|
||||
"title": "Regular document without file",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
document = Document.objects.get()
|
||||
assert document.title == "Regular document without file"
|
||||
assert document.content is None
|
||||
assert document.accesses.filter(role="owner", user=user).exists()
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_file_null_value(mock_convert):
|
||||
"""
|
||||
Passing file=null should be treated as no file upload.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/documents/",
|
||||
{
|
||||
"title": "Document with null file",
|
||||
"file": None,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
document = Document.objects.get()
|
||||
assert document.title == "Document with null file"
|
||||
# Converter should not have been called
|
||||
mock_convert.assert_not_called()
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_file_preserves_content_format(mock_convert):
|
||||
"""
|
||||
Verify that the converted content is stored correctly in the document.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Mock the conversion with realistic base64-encoded YJS data
|
||||
converted_yjs = "AQMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fICA="
|
||||
mock_convert.return_value = converted_yjs
|
||||
|
||||
# Create a fake DOCX file
|
||||
file_content = b"fake docx with complex formatting"
|
||||
file = BytesIO(file_content)
|
||||
file.name = "complex_document.docx"
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/documents/",
|
||||
{
|
||||
"file": file,
|
||||
},
|
||||
format="multipart",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
document = Document.objects.get()
|
||||
|
||||
# Verify the content is stored as returned by the converter
|
||||
assert document.content == converted_yjs
|
||||
|
||||
# Verify it's valid base64 (can be decoded)
|
||||
try:
|
||||
b64decode(converted_yjs)
|
||||
except binascii.Error:
|
||||
pytest.fail("Content should be valid base64-encoded data")
|
||||
|
||||
|
||||
@patch("core.services.converter_services.Converter.convert")
|
||||
def test_api_documents_create_with_file_unicode_filename(mock_convert):
|
||||
"""
|
||||
Test that Unicode characters in filenames are handled correctly.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Mock the conversion
|
||||
converted_yjs = "base64encodedyjscontent"
|
||||
mock_convert.return_value = converted_yjs
|
||||
|
||||
# Create a file with Unicode characters in the name
|
||||
file_content = b"fake docx content"
|
||||
file = BytesIO(file_content)
|
||||
file.name = "文档-télécharger-документ.docx"
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/documents/",
|
||||
{
|
||||
"file": file,
|
||||
},
|
||||
format="multipart",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
document = Document.objects.get()
|
||||
assert document.title == "文档-télécharger-документ.docx"
|
||||
@@ -24,6 +24,7 @@ pytestmark = pytest.mark.django_db
|
||||
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY=True,
|
||||
CRISP_WEBSITE_ID="123",
|
||||
FRONTEND_CSS_URL="http://testcss/",
|
||||
FRONTEND_JS_URL="http://testjs/",
|
||||
FRONTEND_THEME="test-theme",
|
||||
MEDIA_BASE_URL="http://testserver/",
|
||||
POSTHOG_KEY={"id": "132456", "host": "https://eu.i.posthog-test.com"},
|
||||
@@ -49,6 +50,7 @@ def test_api_config(is_authenticated):
|
||||
"ENVIRONMENT": "test",
|
||||
"FRONTEND_CSS_URL": "http://testcss/",
|
||||
"FRONTEND_HOMEPAGE_FEATURE_ENABLED": True,
|
||||
"FRONTEND_JS_URL": "http://testjs/",
|
||||
"FRONTEND_THEME": "test-theme",
|
||||
"LANGUAGES": [
|
||||
["en-us", "English"],
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Test Converter orchestration services."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from core.services import mime_types
|
||||
from core.services.converter_services import Converter
|
||||
|
||||
|
||||
@patch("core.services.converter_services.DocSpecConverter")
|
||||
@patch("core.services.converter_services.YdocConverter")
|
||||
def test_converter_docx_to_yjs_orchestration(mock_ydoc_class, mock_docspec_class):
|
||||
"""Test that DOCX to YJS conversion uses both DocSpec and Ydoc converters."""
|
||||
# Setup mocks
|
||||
mock_docspec = MagicMock()
|
||||
mock_ydoc = MagicMock()
|
||||
mock_docspec_class.return_value = mock_docspec
|
||||
mock_ydoc_class.return_value = mock_ydoc
|
||||
|
||||
# Mock the conversion chain: DOCX -> BlockNote -> YJS
|
||||
blocknote_data = b'[{"type": "paragraph", "content": "test"}]'
|
||||
yjs_data = "base64encodedyjs"
|
||||
|
||||
mock_docspec.convert.return_value = blocknote_data
|
||||
mock_ydoc.convert.return_value = yjs_data
|
||||
|
||||
# Execute conversion
|
||||
converter = Converter()
|
||||
docx_data = b"fake docx data"
|
||||
result = converter.convert(docx_data, mime_types.DOCX, mime_types.YJS)
|
||||
|
||||
# Verify the orchestration
|
||||
mock_docspec.convert.assert_called_once_with(
|
||||
docx_data, mime_types.DOCX, mime_types.BLOCKNOTE
|
||||
)
|
||||
mock_ydoc.convert.assert_called_once_with(
|
||||
blocknote_data, mime_types.BLOCKNOTE, mime_types.YJS
|
||||
)
|
||||
assert result == yjs_data
|
||||
|
||||
|
||||
@patch("core.services.converter_services.YdocConverter")
|
||||
def test_converter_markdown_to_yjs_delegation(mock_ydoc_class):
|
||||
"""Test that Markdown to YJS conversion is delegated to YdocConverter."""
|
||||
mock_ydoc = MagicMock()
|
||||
mock_ydoc_class.return_value = mock_ydoc
|
||||
|
||||
yjs_data = "base64encodedyjs"
|
||||
mock_ydoc.convert.return_value = yjs_data
|
||||
|
||||
converter = Converter()
|
||||
markdown_data = "# Test Document"
|
||||
result = converter.convert(markdown_data, mime_types.MARKDOWN, mime_types.YJS)
|
||||
|
||||
mock_ydoc.convert.assert_called_once_with(
|
||||
markdown_data, mime_types.MARKDOWN, mime_types.YJS
|
||||
)
|
||||
assert result == yjs_data
|
||||
|
||||
|
||||
@patch("core.services.converter_services.YdocConverter")
|
||||
def test_converter_yjs_to_html_delegation(mock_ydoc_class):
|
||||
"""Test that YJS to HTML conversion is delegated to YdocConverter."""
|
||||
mock_ydoc = MagicMock()
|
||||
mock_ydoc_class.return_value = mock_ydoc
|
||||
|
||||
html_data = "<p>Test Document</p>"
|
||||
mock_ydoc.convert.return_value = html_data
|
||||
|
||||
converter = Converter()
|
||||
yjs_data = b"yjs binary data"
|
||||
result = converter.convert(yjs_data, mime_types.YJS, mime_types.HTML)
|
||||
|
||||
mock_ydoc.convert.assert_called_once_with(yjs_data, mime_types.YJS, mime_types.HTML)
|
||||
assert result == html_data
|
||||
|
||||
|
||||
@patch("core.services.converter_services.YdocConverter")
|
||||
def test_converter_blocknote_to_yjs_delegation(mock_ydoc_class):
|
||||
"""Test that BlockNote to YJS conversion is delegated to YdocConverter."""
|
||||
mock_ydoc = MagicMock()
|
||||
mock_ydoc_class.return_value = mock_ydoc
|
||||
|
||||
yjs_data = "base64encodedyjs"
|
||||
mock_ydoc.convert.return_value = yjs_data
|
||||
|
||||
converter = Converter()
|
||||
blocknote_data = b'[{"type": "paragraph"}]'
|
||||
result = converter.convert(blocknote_data, mime_types.BLOCKNOTE, mime_types.YJS)
|
||||
|
||||
mock_ydoc.convert.assert_called_once_with(
|
||||
blocknote_data, mime_types.BLOCKNOTE, mime_types.YJS
|
||||
)
|
||||
assert result == yjs_data
|
||||
@@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from core.services import mime_types
|
||||
from core.services.converter_services import (
|
||||
ServiceUnavailableError,
|
||||
ValidationError,
|
||||
@@ -21,9 +22,9 @@ def test_auth_header(settings):
|
||||
|
||||
|
||||
def test_convert_empty_text():
|
||||
"""Should raise ValidationError when text is empty."""
|
||||
"""Should raise ValidationError when data is empty."""
|
||||
converter = YdocConverter()
|
||||
with pytest.raises(ValidationError, match="Input text cannot be empty"):
|
||||
with pytest.raises(ValidationError, match="Input data cannot be empty"):
|
||||
converter.convert("")
|
||||
|
||||
|
||||
@@ -36,7 +37,7 @@ def test_convert_service_unavailable(mock_post):
|
||||
|
||||
with pytest.raises(
|
||||
ServiceUnavailableError,
|
||||
match="Failed to connect to conversion service",
|
||||
match="Failed to connect to YDoc conversion service",
|
||||
):
|
||||
converter.convert("test text")
|
||||
|
||||
@@ -52,7 +53,7 @@ def test_convert_http_error(mock_post):
|
||||
|
||||
with pytest.raises(
|
||||
ServiceUnavailableError,
|
||||
match="Failed to connect to conversion service",
|
||||
match="Failed to connect to YDoc conversion service",
|
||||
):
|
||||
converter.convert("test text")
|
||||
|
||||
@@ -83,8 +84,8 @@ def test_convert_full_integration(mock_post, settings):
|
||||
data="test markdown",
|
||||
headers={
|
||||
"Authorization": "Bearer test-key",
|
||||
"Content-Type": "text/markdown",
|
||||
"Accept": "application/vnd.yjs.doc",
|
||||
"Content-Type": mime_types.MARKDOWN,
|
||||
"Accept": mime_types.YJS,
|
||||
},
|
||||
timeout=5,
|
||||
verify=False,
|
||||
@@ -108,9 +109,7 @@ def test_convert_full_integration_with_specific_headers(mock_post, settings):
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
result = converter.convert(
|
||||
b"test_content", "application/vnd.yjs.doc", "text/markdown"
|
||||
)
|
||||
result = converter.convert(b"test_content", mime_types.YJS, mime_types.MARKDOWN)
|
||||
|
||||
assert result == expected_response
|
||||
mock_post.assert_called_once_with(
|
||||
@@ -118,8 +117,8 @@ def test_convert_full_integration_with_specific_headers(mock_post, settings):
|
||||
data=b"test_content",
|
||||
headers={
|
||||
"Authorization": "Bearer test-key",
|
||||
"Content-Type": "application/vnd.yjs.doc",
|
||||
"Accept": "text/markdown",
|
||||
"Content-Type": mime_types.YJS,
|
||||
"Accept": mime_types.MARKDOWN,
|
||||
},
|
||||
timeout=5,
|
||||
verify=False,
|
||||
@@ -135,7 +134,7 @@ def test_convert_timeout(mock_post):
|
||||
|
||||
with pytest.raises(
|
||||
ServiceUnavailableError,
|
||||
match="Failed to connect to conversion service",
|
||||
match="Failed to connect to YDoc conversion service",
|
||||
):
|
||||
converter.convert("test text")
|
||||
|
||||
@@ -144,5 +143,5 @@ def test_convert_none_input():
|
||||
"""Should raise ValidationError when input is None."""
|
||||
converter = YdocConverter()
|
||||
|
||||
with pytest.raises(ValidationError, match="Input text cannot be empty"):
|
||||
with pytest.raises(ValidationError, match="Input data cannot be empty"):
|
||||
converter.convert(None)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Test DocSpec converter services."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from core.services import mime_types
|
||||
from core.services.converter_services import (
|
||||
DocSpecConverter,
|
||||
ServiceUnavailableError,
|
||||
ValidationError,
|
||||
)
|
||||
|
||||
|
||||
def test_docspec_convert_empty_data():
|
||||
"""Should raise ValidationError when data is empty."""
|
||||
converter = DocSpecConverter()
|
||||
with pytest.raises(ValidationError, match="Input data cannot be empty"):
|
||||
converter.convert("", mime_types.DOCX, mime_types.BLOCKNOTE)
|
||||
|
||||
|
||||
def test_docspec_convert_none_input():
|
||||
"""Should raise ValidationError when input is None."""
|
||||
converter = DocSpecConverter()
|
||||
with pytest.raises(ValidationError, match="Input data cannot be empty"):
|
||||
converter.convert(None, mime_types.DOCX, mime_types.BLOCKNOTE)
|
||||
|
||||
|
||||
def test_docspec_convert_unsupported_content_type():
|
||||
"""Should raise ValidationError when content type is not DOCX."""
|
||||
converter = DocSpecConverter()
|
||||
with pytest.raises(
|
||||
ValidationError, match="Conversion from text/plain to .* is not supported"
|
||||
):
|
||||
converter.convert(b"test data", "text/plain", mime_types.BLOCKNOTE)
|
||||
|
||||
|
||||
def test_docspec_convert_unsupported_accept():
|
||||
"""Should raise ValidationError when accept type is not BLOCKNOTE."""
|
||||
converter = DocSpecConverter()
|
||||
with pytest.raises(
|
||||
ValidationError,
|
||||
match=f"Conversion from {mime_types.DOCX} to {mime_types.YJS} is not supported",
|
||||
):
|
||||
converter.convert(b"test data", mime_types.DOCX, mime_types.YJS)
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_docspec_convert_service_unavailable(mock_post):
|
||||
"""Should raise ServiceUnavailableError when service is unavailable."""
|
||||
converter = DocSpecConverter()
|
||||
mock_post.side_effect = requests.RequestException("Connection error")
|
||||
|
||||
with pytest.raises(
|
||||
ServiceUnavailableError,
|
||||
match="Failed to connect to DocSpec conversion service",
|
||||
):
|
||||
converter.convert(b"test data", mime_types.DOCX, mime_types.BLOCKNOTE)
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_docspec_convert_http_error(mock_post):
|
||||
"""Should raise ServiceUnavailableError when HTTP error occurs."""
|
||||
converter = DocSpecConverter()
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status.side_effect = requests.HTTPError("HTTP Error")
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
with pytest.raises(
|
||||
ServiceUnavailableError,
|
||||
match="Failed to connect to DocSpec conversion service",
|
||||
):
|
||||
converter.convert(b"test data", mime_types.DOCX, mime_types.BLOCKNOTE)
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_docspec_convert_timeout(mock_post):
|
||||
"""Should raise ServiceUnavailableError when request times out."""
|
||||
converter = DocSpecConverter()
|
||||
mock_post.side_effect = requests.Timeout("Request timed out")
|
||||
|
||||
with pytest.raises(
|
||||
ServiceUnavailableError,
|
||||
match="Failed to connect to DocSpec conversion service",
|
||||
):
|
||||
converter.convert(b"test data", mime_types.DOCX, mime_types.BLOCKNOTE)
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_docspec_convert_success(mock_post, settings):
|
||||
"""Test successful DOCX to BlockNote conversion."""
|
||||
settings.DOCSPEC_API_URL = "http://docspec.test/convert"
|
||||
settings.CONVERSION_API_TIMEOUT = 5
|
||||
settings.CONVERSION_API_SECURE = False
|
||||
|
||||
converter = DocSpecConverter()
|
||||
|
||||
expected_content = b'[{"type": "paragraph", "content": "test"}]'
|
||||
mock_response = MagicMock()
|
||||
mock_response.content = expected_content
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
docx_data = b"fake docx binary data"
|
||||
result = converter.convert(docx_data, mime_types.DOCX, mime_types.BLOCKNOTE)
|
||||
|
||||
assert result == expected_content
|
||||
|
||||
# Verify the request was made correctly
|
||||
mock_post.assert_called_once_with(
|
||||
"http://docspec.test/convert",
|
||||
headers={"Accept": mime_types.BLOCKNOTE},
|
||||
files={"file": ("document.docx", docx_data, mime_types.DOCX)},
|
||||
timeout=5,
|
||||
verify=False,
|
||||
)
|
||||
@@ -509,6 +509,9 @@ class Base(Configuration):
|
||||
FRONTEND_CSS_URL = values.Value(
|
||||
None, environ_name="FRONTEND_CSS_URL", environ_prefix=None
|
||||
)
|
||||
FRONTEND_JS_URL = values.Value(
|
||||
None, environ_name="FRONTEND_JS_URL", environ_prefix=None
|
||||
)
|
||||
|
||||
THEME_CUSTOMIZATION_FILE_PATH = values.Value(
|
||||
os.path.join(BASE_DIR, "impress/configuration/theme/default.json"),
|
||||
@@ -706,6 +709,9 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# DocSpec API microservice
|
||||
DOCSPEC_API_URL = values.Value(environ_name="DOCSPEC_API_URL", environ_prefix=None)
|
||||
|
||||
# Conversion endpoint
|
||||
CONVERSION_API_ENDPOINT = values.Value(
|
||||
default="convert",
|
||||
@@ -1051,6 +1057,9 @@ class Production(Base):
|
||||
# Privacy
|
||||
SECURE_REFERRER_POLICY = "same-origin"
|
||||
|
||||
# Conversion API: Always verify SSL in production
|
||||
CONVERSION_API_SECURE = True
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
|
||||
"PO-Revision-Date: 2026-01-05 08:21\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Breton\n"
|
||||
"Language: br_FR\n"
|
||||
@@ -79,11 +79,15 @@ msgstr "Doare korf"
|
||||
msgid "Format"
|
||||
msgstr "Stumm"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "eilenn {title}"
|
||||
|
||||
#: build/lib/core/apps.py:12 core/apps.py:12
|
||||
msgid "Impress core application"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
|
||||
#: core/choices.py:43
|
||||
msgid "Reader"
|
||||
@@ -239,8 +243,8 @@ msgstr "implijer"
|
||||
msgid "users"
|
||||
msgstr "implijerien"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
msgid "title"
|
||||
msgstr "titl"
|
||||
|
||||
@@ -256,188 +260,188 @@ msgstr "Restr"
|
||||
msgid "Documents"
|
||||
msgstr "Restroù"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
msgid "Untitled Document"
|
||||
msgstr "Restr hep titl"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} en deus rannet ur restr ganeoc'h!"
|
||||
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, 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:869 core/models.py:869
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#, 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:969 core/models.py:969
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Roud liamm ar restr/an implijer"
|
||||
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Roudoù liamm ar restr/an implijer"
|
||||
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
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:999 core/models.py:999
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "Document favorite"
|
||||
msgstr "Restr muiañ-karet"
|
||||
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "Document favorites"
|
||||
msgstr "Restroù muiañ-karet"
|
||||
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
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:1028 core/models.py:1028
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "Document/user relation"
|
||||
msgstr "Liamm restr/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "Document/user relations"
|
||||
msgstr "Liammoù restr/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This user is already in this document."
|
||||
msgstr "An implijer-mañ a zo dija er restr-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
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:1198 core/models.py:1198
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
msgid "Document ask for access"
|
||||
msgstr "Goulenn tizhout ar restr"
|
||||
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Goulennoù tizhout ar restr"
|
||||
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
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:1262 core/models.py:1262
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, 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:1266 core/models.py:1266
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, 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:1272 core/models.py:1272
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#, 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:1314 core/models.py:1314
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "description"
|
||||
msgstr "deskrivadur"
|
||||
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
msgid "code"
|
||||
msgstr "kod"
|
||||
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "public"
|
||||
msgstr "publik"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "M'eo foran ar patrom-mañ hag implijus gant n'eus forzh piv."
|
||||
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
msgid "Template"
|
||||
msgstr "Patrom"
|
||||
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
msgid "Templates"
|
||||
msgstr "Patromoù"
|
||||
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
msgid "Template/user relation"
|
||||
msgstr "Liamm patrom/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
msgid "Template/user relations"
|
||||
msgstr "Liammoù patrom/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
msgid "This user is already in this template."
|
||||
msgstr "An implijer-mañ a zo dija er patrom-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
msgid "email address"
|
||||
msgstr "postel"
|
||||
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
msgid "Document invitation"
|
||||
msgstr "Pedadenn d'ur restr"
|
||||
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
msgid "Document invitations"
|
||||
msgstr "Pedadennoù d'ur restr"
|
||||
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
|
||||
"PO-Revision-Date: 2026-01-05 08:21\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -79,11 +79,15 @@ msgstr "Typ"
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "Kopie von {title}"
|
||||
|
||||
#: build/lib/core/apps.py:12 core/apps.py:12
|
||||
msgid "Impress core application"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
|
||||
#: core/choices.py:43
|
||||
msgid "Reader"
|
||||
@@ -239,8 +243,8 @@ msgstr "Benutzer"
|
||||
msgid "users"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
msgid "title"
|
||||
msgstr "Titel"
|
||||
|
||||
@@ -256,188 +260,188 @@ msgstr "Dokument"
|
||||
msgid "Documents"
|
||||
msgstr "Dokumente"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
msgid "Untitled Document"
|
||||
msgstr "Unbenanntes Dokument"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
|
||||
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, 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:869 core/models.py:869
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#, 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:969 core/models.py:969
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
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:999 core/models.py:999
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "Document favorite"
|
||||
msgstr "Dokumentenfavorit"
|
||||
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "Document favorites"
|
||||
msgstr "Dokumentfavoriten"
|
||||
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
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:1028 core/models.py:1028
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "Document/user relation"
|
||||
msgstr "Dokument/Benutzerbeziehung"
|
||||
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "Document/user relations"
|
||||
msgstr "Dokument/Benutzerbeziehungen"
|
||||
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
|
||||
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
|
||||
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
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:1198 core/models.py:1198
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "description"
|
||||
msgstr "Beschreibung"
|
||||
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
msgid "code"
|
||||
msgstr "Code"
|
||||
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "css"
|
||||
msgstr "CSS"
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "public"
|
||||
msgstr "öffentlich"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
|
||||
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
msgid "Template"
|
||||
msgstr "Vorlage"
|
||||
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
msgid "Templates"
|
||||
msgstr "Vorlagen"
|
||||
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
msgid "Template/user relation"
|
||||
msgstr "Vorlage/Benutzer-Beziehung"
|
||||
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
msgid "Template/user relations"
|
||||
msgstr "Vorlage/Benutzerbeziehungen"
|
||||
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
|
||||
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Dieses Team ist bereits in diesem Template."
|
||||
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
msgid "email address"
|
||||
msgstr "E-Mail-Adresse"
|
||||
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
msgid "Document invitation"
|
||||
msgstr "Einladung zum Dokument"
|
||||
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
msgid "Document invitations"
|
||||
msgstr "Dokumenteinladungen"
|
||||
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
|
||||
"PO-Revision-Date: 2026-01-05 08:21\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_US\n"
|
||||
@@ -79,11 +79,15 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/apps.py:12 core/apps.py:12
|
||||
msgid "Impress core application"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
|
||||
#: core/choices.py:43
|
||||
msgid "Reader"
|
||||
@@ -239,8 +243,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -256,188 +260,188 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
|
||||
"PO-Revision-Date: 2026-01-05 08:21\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Language: es_ES\n"
|
||||
@@ -79,11 +79,15 @@ msgstr "Tipo de Cuerpo"
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copia de {title}"
|
||||
|
||||
#: build/lib/core/apps.py:12 core/apps.py:12
|
||||
msgid "Impress core application"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
|
||||
#: core/choices.py:43
|
||||
msgid "Reader"
|
||||
@@ -239,8 +243,8 @@ msgstr "usuario"
|
||||
msgid "users"
|
||||
msgstr "usuarios"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
msgid "title"
|
||||
msgstr "título"
|
||||
|
||||
@@ -256,188 +260,188 @@ msgstr "Documento"
|
||||
msgid "Documents"
|
||||
msgstr "Documentos"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
msgid "Untitled Document"
|
||||
msgstr "Documento sin título"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "¡{name} ha compartido un documento contigo!"
|
||||
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, 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:869 core/models.py:869
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} ha compartido un documento contigo: {title}"
|
||||
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Traza del enlace de documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Trazas del enlace de documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
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:999 core/models.py:999
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "Document favorite"
|
||||
msgstr "Documento favorito"
|
||||
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "Document favorites"
|
||||
msgstr "Documentos favoritos"
|
||||
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
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:1028 core/models.py:1028
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "Document/user relation"
|
||||
msgstr "Relación documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "Document/user relations"
|
||||
msgstr "Relaciones documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Este usuario ya forma parte del documento."
|
||||
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Este equipo ya forma parte del documento."
|
||||
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
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:1198 core/models.py:1198
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
msgid "Document ask for access"
|
||||
msgstr "Solicitud de acceso"
|
||||
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Solicitud de accesos"
|
||||
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
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:1262 core/models.py:1262
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "¡{name} desea acceder a un documento!"
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} desea acceso al siguiente documento:"
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#, 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:1314 core/models.py:1314
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "description"
|
||||
msgstr "descripción"
|
||||
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
msgid "code"
|
||||
msgstr "código"
|
||||
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "public"
|
||||
msgstr "público"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Si esta plantilla es pública para que cualquiera la utilice."
|
||||
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
msgid "Template"
|
||||
msgstr "Plantilla"
|
||||
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
msgid "Templates"
|
||||
msgstr "Plantillas"
|
||||
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
msgid "Template/user relation"
|
||||
msgstr "Relación plantilla/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
msgid "Template/user relations"
|
||||
msgstr "Relaciones plantilla/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Este usuario ya forma parte de la plantilla."
|
||||
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Este equipo ya se encuentra en esta plantilla."
|
||||
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
msgid "email address"
|
||||
msgstr "dirección de correo electrónico"
|
||||
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
msgid "Document invitation"
|
||||
msgstr "Invitación al documento"
|
||||
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
msgid "Document invitations"
|
||||
msgstr "Invitaciones a documentos"
|
||||
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Este correo electrónico está asociado a un usuario registrado."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
|
||||
"PO-Revision-Date: 2026-01-05 08:21\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr_FR\n"
|
||||
@@ -79,11 +79,15 @@ msgstr "Type de corps"
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copie de {title}"
|
||||
|
||||
#: build/lib/core/apps.py:12 core/apps.py:12
|
||||
msgid "Impress core application"
|
||||
msgstr "Noyau d'application Impress"
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
|
||||
#: core/choices.py:43
|
||||
msgid "Reader"
|
||||
@@ -239,8 +243,8 @@ msgstr "utilisateur"
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
msgid "title"
|
||||
msgstr "titre"
|
||||
|
||||
@@ -256,188 +260,188 @@ msgstr "Document"
|
||||
msgid "Documents"
|
||||
msgstr "Documents"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
msgid "Untitled Document"
|
||||
msgstr "Document sans titre"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} a partagé un document avec vous!"
|
||||
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, 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:869 core/models.py:869
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#, 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:969 core/models.py:969
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Trace du lien document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Traces du lien document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
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:999 core/models.py:999
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "Document favorite"
|
||||
msgstr "Document favori"
|
||||
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "Document favorites"
|
||||
msgstr "Documents favoris"
|
||||
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
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:1028 core/models.py:1028
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "Document/user relation"
|
||||
msgstr "Relation document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "Document/user relations"
|
||||
msgstr "Relations document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Cet utilisateur est déjà dans ce document."
|
||||
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Cette équipe est déjà dans ce document."
|
||||
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
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:1198 core/models.py:1198
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
msgid "Document ask for access"
|
||||
msgstr "Demande d'accès au document"
|
||||
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Demande d'accès au document"
|
||||
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
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:1262 core/models.py:1262
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} souhaiterait accéder au document suivant !"
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, 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:1272 core/models.py:1272
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#, 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:1314 core/models.py:1314
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
msgid "Thread"
|
||||
msgstr "Conversation"
|
||||
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
msgid "Threads"
|
||||
msgstr "Conversations"
|
||||
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
msgid "Anonymous"
|
||||
msgstr "Anonyme"
|
||||
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
msgid "Comment"
|
||||
msgstr "Commentaire"
|
||||
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
msgid "Comments"
|
||||
msgstr "Commentaires"
|
||||
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
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:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
msgid "Reaction"
|
||||
msgstr "Réaction"
|
||||
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
msgid "Reactions"
|
||||
msgstr "Réactions"
|
||||
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "description"
|
||||
msgstr "description"
|
||||
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "css"
|
||||
msgstr "CSS"
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "public"
|
||||
msgstr "public"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Si ce modèle est public, utilisable par n'importe qui."
|
||||
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
msgid "Template"
|
||||
msgstr "Modèle"
|
||||
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
msgid "Templates"
|
||||
msgstr "Modèles"
|
||||
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
msgid "Template/user relation"
|
||||
msgstr "Relation modèle/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
msgid "Template/user relations"
|
||||
msgstr "Relations modèle/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Cet utilisateur est déjà dans ce modèle."
|
||||
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Cette équipe est déjà modèle."
|
||||
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
msgid "email address"
|
||||
msgstr "adresse e-mail"
|
||||
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
msgid "Document invitation"
|
||||
msgstr "Invitation à un document"
|
||||
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
msgid "Document invitations"
|
||||
msgstr "Invitations à un document"
|
||||
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
|
||||
"PO-Revision-Date: 2026-01-05 08:21\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Italian\n"
|
||||
"Language: it_IT\n"
|
||||
@@ -79,11 +79,15 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copia di {title}"
|
||||
|
||||
#: build/lib/core/apps.py:12 core/apps.py:12
|
||||
msgid "Impress core application"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
|
||||
#: core/choices.py:43
|
||||
msgid "Reader"
|
||||
@@ -239,8 +243,8 @@ msgstr "utente"
|
||||
msgid "users"
|
||||
msgstr "utenti"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
msgid "title"
|
||||
msgstr "titolo"
|
||||
|
||||
@@ -256,188 +260,188 @@ msgstr "Documento"
|
||||
msgid "Documents"
|
||||
msgstr "Documenti"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
msgid "Untitled Document"
|
||||
msgstr "Documento senza titolo"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} ha condiviso un documento con te!"
|
||||
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, 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:869 core/models.py:869
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#, 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:969 core/models.py:969
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "Document favorite"
|
||||
msgstr "Documento preferito"
|
||||
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "Document favorites"
|
||||
msgstr "Documenti preferiti"
|
||||
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Questo utente è già presente in questo documento."
|
||||
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Questo team è già presente in questo documento."
|
||||
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "description"
|
||||
msgstr "descrizione"
|
||||
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "public"
|
||||
msgstr "pubblico"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Indica se questo modello è pubblico per chiunque."
|
||||
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
msgid "Template"
|
||||
msgstr "Modello"
|
||||
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
msgid "Templates"
|
||||
msgstr "Modelli"
|
||||
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Questo utente è già in questo modello."
|
||||
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Questo team è già in questo modello."
|
||||
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
msgid "email address"
|
||||
msgstr "indirizzo e-mail"
|
||||
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
msgid "Document invitation"
|
||||
msgstr "Invito al documento"
|
||||
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
msgid "Document invitations"
|
||||
msgstr "Inviti al documento"
|
||||
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Questa email è già associata a un utente registrato."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
|
||||
"PO-Revision-Date: 2026-01-05 08:21\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Language: nl_NL\n"
|
||||
@@ -79,11 +79,15 @@ msgstr "Text type"
|
||||
msgid "Format"
|
||||
msgstr "Formaat"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "kopie van {title}"
|
||||
|
||||
#: build/lib/core/apps.py:12 core/apps.py:12
|
||||
msgid "Impress core application"
|
||||
msgstr "Docs kern applicatie"
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
|
||||
#: core/choices.py:43
|
||||
msgid "Reader"
|
||||
@@ -239,8 +243,8 @@ msgstr "gebruiker"
|
||||
msgid "users"
|
||||
msgstr "gebruikers"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
msgid "title"
|
||||
msgstr "titel"
|
||||
|
||||
@@ -256,188 +260,188 @@ msgstr "Document"
|
||||
msgid "Documents"
|
||||
msgstr "Documenten"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
msgid "Untitled Document"
|
||||
msgstr "Naamloos Document"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} heeft een document met u gedeeld!"
|
||||
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, 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:869 core/models.py:869
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#, 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:969 core/models.py:969
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Document/gebruiker link"
|
||||
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Document/gebruiker link"
|
||||
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
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:999 core/models.py:999
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "Document favorite"
|
||||
msgstr "Document favoriet"
|
||||
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "Document favorites"
|
||||
msgstr "Document favorieten"
|
||||
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
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:1028 core/models.py:1028
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "Document/user relation"
|
||||
msgstr "Document/gebruiker relatie"
|
||||
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "Document/user relations"
|
||||
msgstr "Document/gebruiker relaties"
|
||||
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This user is already in this document."
|
||||
msgstr "De gebruiker bestaat al in dit document."
|
||||
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Dit team bestaat al in dit document."
|
||||
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
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:1198 core/models.py:1198
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
msgid "Document ask for access"
|
||||
msgstr "Document verzoekt om toegang"
|
||||
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Document verzoekt om toegangen"
|
||||
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
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:1262 core/models.py:1262
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} verzoekt toegang tot een document!"
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, 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:1272 core/models.py:1272
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#, 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:1314 core/models.py:1314
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
msgid "Thread"
|
||||
msgstr "Kanaal"
|
||||
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
msgid "Threads"
|
||||
msgstr "Kanalen"
|
||||
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
msgid "Anonymous"
|
||||
msgstr "Anoniem"
|
||||
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
msgid "Comment"
|
||||
msgstr "Reactie"
|
||||
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
msgid "Comments"
|
||||
msgstr "Reacties"
|
||||
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr "Deze emoji is al op deze opmerking gereageerd."
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
msgid "Reaction"
|
||||
msgstr "Reactie"
|
||||
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
msgid "Reactions"
|
||||
msgstr "Reacties"
|
||||
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "description"
|
||||
msgstr "omschrijving"
|
||||
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "public"
|
||||
msgstr "publiek"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Of dit sjabloon door iedereen publiekelijk te gebruiken is."
|
||||
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
msgid "Template"
|
||||
msgstr "Sjabloon"
|
||||
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
msgid "Templates"
|
||||
msgstr "Sjabloon"
|
||||
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
msgid "Template/user relation"
|
||||
msgstr "Sjabloon/gebruiker relatie"
|
||||
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
msgid "Template/user relations"
|
||||
msgstr "Sjabloon/gebruiker relaties"
|
||||
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
msgid "This user is already in this template."
|
||||
msgstr "De gebruiker bestaat al in dit sjabloon."
|
||||
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Het team bestaat al in dit sjabloon."
|
||||
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
msgid "email address"
|
||||
msgstr "e-mailadres"
|
||||
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
msgid "Document invitation"
|
||||
msgstr "Document uitnodiging"
|
||||
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
msgid "Document invitations"
|
||||
msgstr "Document uitnodigingen"
|
||||
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
|
||||
"PO-Revision-Date: 2026-01-05 08:21\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Portuguese\n"
|
||||
"Language: pt_PT\n"
|
||||
@@ -79,11 +79,15 @@ msgstr "Tipo de corpo"
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "cópia de {title}"
|
||||
|
||||
#: build/lib/core/apps.py:12 core/apps.py:12
|
||||
msgid "Impress core application"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
|
||||
#: core/choices.py:43
|
||||
msgid "Reader"
|
||||
@@ -239,8 +243,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -256,188 +260,188 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
|
||||
"PO-Revision-Date: 2026-01-05 08:21\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Russian\n"
|
||||
"Language: ru_RU\n"
|
||||
@@ -79,11 +79,15 @@ msgstr "Тип сообщения"
|
||||
msgid "Format"
|
||||
msgstr "Формат"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
|
||||
#, 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"
|
||||
@@ -239,8 +243,8 @@ msgstr "пользователь"
|
||||
msgid "users"
|
||||
msgstr "пользователи"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
msgid "title"
|
||||
msgstr "заголовок"
|
||||
|
||||
@@ -256,188 +260,188 @@ msgstr "Документ"
|
||||
msgid "Documents"
|
||||
msgstr "Документы"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
msgid "Untitled Document"
|
||||
msgstr "Безымянный документ"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} делится с вами документом!"
|
||||
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
|
||||
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} делится с вами документом: {title}"
|
||||
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Трассировка связи документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Трассировка связей документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
|
||||
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "Document favorite"
|
||||
msgstr "Избранный документ"
|
||||
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "Document favorites"
|
||||
msgstr "Избранные документы"
|
||||
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Этот документ уже помечен как избранный для этого пользователя."
|
||||
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "Document/user relation"
|
||||
msgstr "Отношение документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "Document/user relations"
|
||||
msgstr "Отношения документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Этот пользователь уже имеет доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Эта команда уже имеет доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
|
||||
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
msgid "Document ask for access"
|
||||
msgstr "Документ запрашивает доступ"
|
||||
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Документ запрашивает доступы"
|
||||
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "Этот пользователь уже запросил доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} хочет получить доступ к документу!"
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} хочет получить доступ к следующему документу:"
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name} запрашивает доступ к документу: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
msgid "Thread"
|
||||
msgstr "Обсуждение"
|
||||
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
msgid "Threads"
|
||||
msgstr "Обсуждения"
|
||||
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
msgid "Anonymous"
|
||||
msgstr "Аноним"
|
||||
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
msgid "Comment"
|
||||
msgstr "Комментарий"
|
||||
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
msgid "Comments"
|
||||
msgstr "Комментарии"
|
||||
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr "Этот эмодзи уже использован в этом комментарии."
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
msgid "Reaction"
|
||||
msgstr "Реакция"
|
||||
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
msgid "Reactions"
|
||||
msgstr "Реакции"
|
||||
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "description"
|
||||
msgstr "описание"
|
||||
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
msgid "code"
|
||||
msgstr "код"
|
||||
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "public"
|
||||
msgstr "доступно всем"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Этот шаблон доступен всем пользователям."
|
||||
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
msgid "Template"
|
||||
msgstr "Шаблон"
|
||||
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
msgid "Templates"
|
||||
msgstr "Шаблоны"
|
||||
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
msgid "Template/user relation"
|
||||
msgstr "Отношение шаблон/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
msgid "Template/user relations"
|
||||
msgstr "Отношения шаблон/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Этот пользователь уже указан в этом шаблоне."
|
||||
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Эта команда уже указана в этом шаблоне."
|
||||
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
msgid "email address"
|
||||
msgstr "адрес электронной почты"
|
||||
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
msgid "Document invitation"
|
||||
msgstr "Приглашение для документа"
|
||||
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
msgid "Document invitations"
|
||||
msgstr "Приглашения для документов"
|
||||
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Этот адрес уже связан с зарегистрированным пользователем."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
|
||||
"PO-Revision-Date: 2026-01-05 08:21\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Slovenian\n"
|
||||
"Language: sl_SI\n"
|
||||
@@ -79,11 +79,15 @@ msgstr "Vrsta telesa"
|
||||
msgid "Format"
|
||||
msgstr "Oblika"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/apps.py:12 core/apps.py:12
|
||||
msgid "Impress core application"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
|
||||
#: core/choices.py:43
|
||||
msgid "Reader"
|
||||
@@ -239,8 +243,8 @@ msgstr "uporabnik"
|
||||
msgid "users"
|
||||
msgstr "uporabniki"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
msgid "title"
|
||||
msgstr "naslov"
|
||||
|
||||
@@ -256,188 +260,188 @@ msgstr "Dokument"
|
||||
msgid "Documents"
|
||||
msgstr "Dokumenti"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
msgid "Untitled Document"
|
||||
msgstr "Dokument brez naslova"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} je delil dokument z vami!"
|
||||
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, 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:869 core/models.py:869
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} je delil dokument z vami: {title}"
|
||||
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Dokument/sled povezave uporabnika"
|
||||
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Sledi povezav dokumenta/uporabnika"
|
||||
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
|
||||
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "Document favorite"
|
||||
msgstr "Priljubljeni dokument"
|
||||
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "Document favorites"
|
||||
msgstr "Priljubljeni dokumenti"
|
||||
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
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:1028 core/models.py:1028
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "Document/user relation"
|
||||
msgstr "Odnos dokument/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "Document/user relations"
|
||||
msgstr "Odnosi dokument/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Ta uporabnik je že v tem dokumentu."
|
||||
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ta ekipa je že v tem dokumentu."
|
||||
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
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:1198 core/models.py:1198
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "description"
|
||||
msgstr "opis"
|
||||
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
msgid "code"
|
||||
msgstr "koda"
|
||||
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "public"
|
||||
msgstr "javno"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Ali je ta predloga javna za uporabo."
|
||||
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
msgid "Template"
|
||||
msgstr "Predloga"
|
||||
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
msgid "Templates"
|
||||
msgstr "Predloge"
|
||||
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
msgid "Template/user relation"
|
||||
msgstr "Odnos predloga/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
msgid "Template/user relations"
|
||||
msgstr "Odnosi med predlogo in uporabnikom"
|
||||
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Ta uporabnik je že v tej predlogi."
|
||||
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Ta ekipa je že v tej predlogi."
|
||||
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
msgid "email address"
|
||||
msgstr "elektronski naslov"
|
||||
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
msgid "Document invitation"
|
||||
msgstr "Vabilo na dokument"
|
||||
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
msgid "Document invitations"
|
||||
msgstr "Vabila na dokument"
|
||||
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
|
||||
"PO-Revision-Date: 2026-01-05 08:21\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Swedish\n"
|
||||
"Language: sv_SE\n"
|
||||
@@ -79,11 +79,15 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/apps.py:12 core/apps.py:12
|
||||
msgid "Impress core application"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
|
||||
#: core/choices.py:43
|
||||
msgid "Reader"
|
||||
@@ -239,8 +243,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -256,188 +260,188 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
msgid "email address"
|
||||
msgstr "e-postadress"
|
||||
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
msgid "Document invitation"
|
||||
msgstr "Bjud in dokument"
|
||||
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
msgid "Document invitations"
|
||||
msgstr "Inbjudningar dokument"
|
||||
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Denna e-postadress är redan associerad med en registrerad användare."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
|
||||
"PO-Revision-Date: 2026-01-05 08:21\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Turkish\n"
|
||||
"Language: tr_TR\n"
|
||||
@@ -79,11 +79,15 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/apps.py:12 core/apps.py:12
|
||||
msgid "Impress core application"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
|
||||
#: core/choices.py:43
|
||||
msgid "Reader"
|
||||
@@ -239,8 +243,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -256,188 +260,188 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
|
||||
"PO-Revision-Date: 2026-01-05 08:21\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Ukrainian\n"
|
||||
"Language: uk_UA\n"
|
||||
@@ -79,11 +79,15 @@ msgstr "Тип вмісту"
|
||||
msgid "Format"
|
||||
msgstr "Формат"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
|
||||
#, 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"
|
||||
@@ -239,8 +243,8 @@ msgstr "користувач"
|
||||
msgid "users"
|
||||
msgstr "користувачі"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
msgid "title"
|
||||
msgstr "заголовок"
|
||||
|
||||
@@ -256,188 +260,188 @@ msgstr "Документ"
|
||||
msgid "Documents"
|
||||
msgstr "Документи"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
msgid "Untitled Document"
|
||||
msgstr "Документ без назви"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} ділиться з вами документом!"
|
||||
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
|
||||
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} ділиться з вами документом: {title}"
|
||||
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Трасування посилання Документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Трасування посилань Документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
|
||||
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "Document favorite"
|
||||
msgstr "Обраний документ"
|
||||
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "Document favorites"
|
||||
msgstr "Обрані документи"
|
||||
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Цей документ вже вказаний як обраний для одного користувача."
|
||||
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "Document/user relation"
|
||||
msgstr "Відносини документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "Document/user relations"
|
||||
msgstr "Відносини документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Цей користувач вже має доступ до цього документу."
|
||||
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ця команда вже має доступ до цього документа."
|
||||
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Вкажіть користувача або команду, а не обох."
|
||||
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
msgid "Document ask for access"
|
||||
msgstr "Запит доступу до документа"
|
||||
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Запит доступу для документа"
|
||||
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "Цей користувач вже попросив доступ до цього документа."
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} хоче отримати доступ до документа!"
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} бажає отримати доступ до наступного документа:"
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name} запитує доступ до документа: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
msgid "Thread"
|
||||
msgstr "Обговорення"
|
||||
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
msgid "Threads"
|
||||
msgstr "Обговорення"
|
||||
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
msgid "Anonymous"
|
||||
msgstr "Анонім"
|
||||
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
msgid "Comment"
|
||||
msgstr "Коментар"
|
||||
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
msgid "Comments"
|
||||
msgstr "Коментарі"
|
||||
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr "Цим емодзі вже відреагували на цей коментар."
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
msgid "Reaction"
|
||||
msgstr "Реакція"
|
||||
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
msgid "Reactions"
|
||||
msgstr "Реакції"
|
||||
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "description"
|
||||
msgstr "опис"
|
||||
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
msgid "code"
|
||||
msgstr "код"
|
||||
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "public"
|
||||
msgstr "публічне"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Чи є цей шаблон публічним для будь-кого користувача."
|
||||
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
msgid "Template"
|
||||
msgstr "Шаблон"
|
||||
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
msgid "Templates"
|
||||
msgstr "Шаблони"
|
||||
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
msgid "Template/user relation"
|
||||
msgstr "Відношення шаблон/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
msgid "Template/user relations"
|
||||
msgstr "Відношення шаблон/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Цей користувач вже має доступ до цього шаблону."
|
||||
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Ця команда вже має доступ до цього шаблону."
|
||||
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
msgid "email address"
|
||||
msgstr "електронна адреса"
|
||||
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
msgid "Document invitation"
|
||||
msgstr "Запрошення до редагування документа"
|
||||
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
msgid "Document invitations"
|
||||
msgstr "Запрошення до редагування документів"
|
||||
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
|
||||
"PO-Revision-Date: 2026-01-05 08:21\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"Language: zh_CN\n"
|
||||
@@ -79,11 +79,15 @@ msgstr "正文类型"
|
||||
msgid "Format"
|
||||
msgstr "格式"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "{title} 的副本"
|
||||
|
||||
#: build/lib/core/apps.py:12 core/apps.py:12
|
||||
msgid "Impress core application"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
|
||||
#: core/choices.py:43
|
||||
msgid "Reader"
|
||||
@@ -239,8 +243,8 @@ msgstr "用户"
|
||||
msgid "users"
|
||||
msgstr "个用户"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
msgid "title"
|
||||
msgstr "标题"
|
||||
|
||||
@@ -256,188 +260,188 @@ msgstr "文档"
|
||||
msgid "Documents"
|
||||
msgstr "个文档"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
msgid "Untitled Document"
|
||||
msgstr "未命名文档"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} 与您共享了一个文档!"
|
||||
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
|
||||
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} 与您共享了一个文档:{title}"
|
||||
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "Document/user link trace"
|
||||
msgstr "文档/用户链接跟踪"
|
||||
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
msgid "Document/user link traces"
|
||||
msgstr "个文档/用户链接跟踪"
|
||||
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "此文档/用户的链接跟踪已存在。"
|
||||
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "Document favorite"
|
||||
msgstr "文档收藏"
|
||||
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
msgid "Document favorites"
|
||||
msgstr "文档收藏夹"
|
||||
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "该文档已被同一用户的收藏关系实例关联。"
|
||||
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "Document/user relation"
|
||||
msgstr "文档/用户关系"
|
||||
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
msgid "Document/user relations"
|
||||
msgstr "文档/用户关系集"
|
||||
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
msgid "This user is already in this document."
|
||||
msgstr "该用户已在此文档中。"
|
||||
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
msgid "This team is already in this document."
|
||||
msgstr "该团队已在此文档中。"
|
||||
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "必须设置用户或团队之一,不能同时设置两者。"
|
||||
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
msgid "Document ask for access"
|
||||
msgstr "文档需要访问权限"
|
||||
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "文档需要访问权限"
|
||||
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "用户已申请该文档的访问权限。"
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} 申请访问文档!"
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} 申请访问以下文档:"
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name}申请文档:{title}的访问权限"
|
||||
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "description"
|
||||
msgstr "说明"
|
||||
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
msgid "code"
|
||||
msgstr "代码"
|
||||
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "public"
|
||||
msgstr "公开"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "该模板是否公开供任何人使用。"
|
||||
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
msgid "Template"
|
||||
msgstr "模板"
|
||||
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
msgid "Templates"
|
||||
msgstr "模板"
|
||||
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
msgid "Template/user relation"
|
||||
msgstr "模板/用户关系"
|
||||
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
msgid "Template/user relations"
|
||||
msgstr "模板/用户关系集"
|
||||
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
msgid "This user is already in this template."
|
||||
msgstr "该用户已在此模板中。"
|
||||
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
msgid "This team is already in this template."
|
||||
msgstr "该团队已在此模板中。"
|
||||
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
msgid "email address"
|
||||
msgstr "电子邮件地址"
|
||||
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
msgid "Document invitation"
|
||||
msgstr "文档邀请"
|
||||
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
msgid "Document invitations"
|
||||
msgstr "文档邀请"
|
||||
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "此电子邮件已经与现有注册用户关联。"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "4.2.0"
|
||||
version = "4.3.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,60 @@
|
||||

|
||||
|
||||
# Lorem Ipsum import Document
|
||||
|
||||
## Introduction
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam auctor, nisl eget ultricies tincidunt, nisl nisl aliquam nisl, eget ultricies nisl nisl eget nisl.
|
||||
|
||||
### Subsection 1.1
|
||||
|
||||
* **Bold text**: Lorem ipsum dolor sit amet.
|
||||
|
||||
* *Italic text*: Consectetur adipiscing elit.
|
||||
|
||||
* ~~Strikethrough text~~: Nullam auctor, nisl eget ultricies tincidunt.
|
||||
|
||||
1. First item in an ordered list.
|
||||
|
||||
2. Second item in an ordered list.
|
||||
|
||||
* Indented bullet point.
|
||||
|
||||
* Another indented bullet point.
|
||||
|
||||
3. Third item in an ordered list.
|
||||
|
||||
### Subsection 1.2
|
||||
|
||||
**Code block:**
|
||||
|
||||
```js
|
||||
const hello_world = () => {
|
||||
console.log("Hello, world!");
|
||||
}
|
||||
```
|
||||
|
||||
**Blockquote:**
|
||||
|
||||
> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam auctor, nisl eget ultricies tincidunt.
|
||||
|
||||
**Horizontal rule:**
|
||||
|
||||
***
|
||||
|
||||
**Table:**
|
||||
|
||||
| Syntax | Description |
|
||||
| --------- | ----------- |
|
||||
| Header | Title |
|
||||
| Paragraph | Text |
|
||||
|
||||
**Inline code:**
|
||||
|
||||
Use the `printf()` function.
|
||||
|
||||
**Link:** [Example](http://localhost:3000/)
|
||||
|
||||
## Conclusion
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam auctor, nisl eget ultricies tincidunt, nisl nisl aliquam nisl, eget ultricies nisl nisl eget nisl.
|
||||
@@ -126,6 +126,20 @@ test.describe('Config', () => {
|
||||
).toBeAttached();
|
||||
});
|
||||
|
||||
test('it checks FRONTEND_JS_URL config', async ({ page }) => {
|
||||
await overrideConfig(page, {
|
||||
FRONTEND_JS_URL: 'http://localhost:123465/js/script.js',
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
await expect(
|
||||
page
|
||||
.locator('script[src="http://localhost:123465/js/script.js"]')
|
||||
.first(),
|
||||
).toBeAttached();
|
||||
});
|
||||
|
||||
test('it checks theme_customization.translations config', async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -145,10 +159,6 @@ test.describe('Config', () => {
|
||||
|
||||
await expect(page.getByText('MyCustomDocs')).toBeAttached();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Config: Not logged', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('it checks the config api is called', async ({ page }) => {
|
||||
const responsePromise = page.waitForResponse(
|
||||
@@ -168,6 +178,10 @@ test.describe('Config: Not logged', () => {
|
||||
|
||||
expect(configApi).toStrictEqual(CONFIG_LEFT);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Config: Not logged', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('it checks that theme is configured from config endpoint', async ({
|
||||
page,
|
||||
|
||||
@@ -153,7 +153,7 @@ test.describe('Doc Comments', () => {
|
||||
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.getByRole('button', { name: 'Save' });
|
||||
const saveBtn = thread.locator('button[data-test="save"]').first();
|
||||
await saveBtn.click();
|
||||
await expect(saveBtn).toBeHidden();
|
||||
await expect(
|
||||
@@ -163,7 +163,8 @@ test.describe('Doc Comments', () => {
|
||||
|
||||
// Add second comment
|
||||
await thread.getByRole('paragraph').last().fill('This is a second comment');
|
||||
await thread.getByRole('button', { name: 'Save' }).click();
|
||||
await saveBtn.click();
|
||||
await expect(saveBtn).toBeHidden();
|
||||
await expect(
|
||||
thread.getByText('This is an edited comment').first(),
|
||||
).toBeVisible();
|
||||
@@ -371,7 +372,7 @@ test.describe('Doc Comments', () => {
|
||||
test.describe('Doc Comments mobile', () => {
|
||||
test.use({ viewport: { width: 500, height: 1200 } });
|
||||
|
||||
test('Comments are not visible on mobile', async ({ page, browserName }) => {
|
||||
test('Can comments on mobile', async ({ page, browserName }) => {
|
||||
const [title] = await createDoc(
|
||||
page,
|
||||
'comment-mobile',
|
||||
@@ -387,7 +388,18 @@ test.describe('Doc Comments mobile', () => {
|
||||
// Checks add react reaction
|
||||
const editor = await writeInEditor({ page, text: 'Hello' });
|
||||
await editor.getByText('Hello').selectText();
|
||||
await expect(page.getByRole('button', { name: 'Comment' })).toBeHidden();
|
||||
await expect(page.getByRole('button', { name: 'Paragraph' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Comment' }).click();
|
||||
|
||||
const thread = page.locator('.bn-thread');
|
||||
await thread.getByRole('paragraph').first().fill('This is a comment');
|
||||
await thread.locator('[data-test="save"]').click();
|
||||
await expect(thread.getByText('This is a comment').first()).toBeHidden();
|
||||
// Check toolbar is closed after adding a comment
|
||||
await expect(page.getByRole('button', { name: 'Paragraph' })).toBeHidden();
|
||||
|
||||
await editor.first().click();
|
||||
await editor.getByText('Hello').click();
|
||||
|
||||
await expect(thread.getByText('This is a comment').first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -604,7 +604,7 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
const editor = await openSuggestionMenu({ page });
|
||||
const { editor } = await openSuggestionMenu({ page });
|
||||
await page.getByText('Embedded file').click();
|
||||
await page.getByText('Upload file').click();
|
||||
|
||||
@@ -880,14 +880,15 @@ test.describe('Doc Editor', () => {
|
||||
// Wait for the interlink to be created and rendered
|
||||
const editor = await getEditor({ page });
|
||||
|
||||
const interlinkChild2 = editor.getByRole('button', {
|
||||
name: docChild2,
|
||||
});
|
||||
const interlinkChild = editor
|
||||
.locator('.--docs--interlinking-link-inline-content')
|
||||
.first();
|
||||
|
||||
await expect(interlinkChild2).toBeVisible({ timeout: 10000 });
|
||||
await expect(interlinkChild2).toContainText('😀');
|
||||
await expect(interlinkChild2.locator('svg').first()).toBeHidden();
|
||||
await interlinkChild2.click();
|
||||
await expect(interlinkChild).toBeVisible({ timeout: 10000 });
|
||||
await expect(interlinkChild).toContainText('😀');
|
||||
await expect(interlinkChild).toContainText(docChild2);
|
||||
await expect(interlinkChild.locator('svg').first()).toBeHidden();
|
||||
await interlinkChild.click();
|
||||
|
||||
await verifyDocName(page, docChild2);
|
||||
|
||||
@@ -897,11 +898,9 @@ test.describe('Doc Editor', () => {
|
||||
await input.fill(docChild1);
|
||||
await searchContainer.getByText(docChild1).click();
|
||||
|
||||
const interlinkChild1 = editor.getByRole('button', {
|
||||
name: docChild1,
|
||||
});
|
||||
await expect(interlinkChild1).toBeVisible({ timeout: 10000 });
|
||||
await expect(interlinkChild1.locator('svg').first()).toBeVisible();
|
||||
await expect(interlinkChild).toContainText(docChild1);
|
||||
await expect(interlinkChild).toBeVisible({ timeout: 10000 });
|
||||
await expect(interlinkChild.locator('svg').first()).toBeVisible();
|
||||
|
||||
await page.keyboard.press('@');
|
||||
|
||||
@@ -961,13 +960,35 @@ test.describe('Doc Editor', () => {
|
||||
test('it embeds PDF', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'doc-toolbar', browserName, 1);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
await updateShareLink(page, 'Public', 'Reading');
|
||||
|
||||
await page.getByRole('button', { name: 'Close the share modal' }).click();
|
||||
|
||||
await openSuggestionMenu({ page });
|
||||
await page.getByText('Embed a PDF file').click();
|
||||
|
||||
const pdfBlock = page.locator('div[data-content-type="pdf"]').first();
|
||||
const pdfBlock = page.locator('div[data-content-type="pdf"]').last();
|
||||
|
||||
await expect(pdfBlock).toBeVisible();
|
||||
|
||||
// Try with invalid PDF first
|
||||
await page.getByText(/Add (PDF|file)/).click();
|
||||
|
||||
await page.locator('[data-test="embed-tab"]').click();
|
||||
|
||||
await page
|
||||
.locator('[data-test="embed-input"]')
|
||||
.fill('https://example.test/test.test');
|
||||
|
||||
await page.locator('[data-test="embed-input-button"]').click();
|
||||
|
||||
await expect(page.getByText('Invalid or missing PDF file')).toBeVisible();
|
||||
|
||||
await openSuggestionMenu({ page });
|
||||
await page.getByText('Embed a PDF file').click();
|
||||
|
||||
// Now with a valid PDF
|
||||
await page.getByText(/Add (PDF|file)/).click();
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
@@ -992,7 +1013,7 @@ test.describe('Doc Editor', () => {
|
||||
await expect(pdfEmbed).toHaveAttribute('role', 'presentation');
|
||||
|
||||
// Check download with original filename
|
||||
await page.locator('.bn-block-content[data-content-type="pdf"]').click();
|
||||
await pdfBlock.click();
|
||||
await page.locator('[data-test="downloadfile"]').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { Download, Page, expect, test } from '@playwright/test';
|
||||
import cs from 'convert-stream';
|
||||
import JSZip from 'jszip';
|
||||
import { PDFParse } from 'pdf-parse';
|
||||
|
||||
import {
|
||||
BrowserName,
|
||||
TestLanguage,
|
||||
createDoc,
|
||||
verifyDocName,
|
||||
waitForLanguageSwitch,
|
||||
} from './utils-common';
|
||||
import { openSuggestionMenu, writeInEditor } from './utils-editor';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -46,81 +47,14 @@ test.describe('Doc Export', () => {
|
||||
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it exports the doc with pdf line break', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [randomDoc] = await createDoc(
|
||||
page,
|
||||
'doc-editor-line-break',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
const editor = await writeInEditor({ page, text: 'Hello' });
|
||||
await page.keyboard.press('Enter');
|
||||
await openSuggestionMenu({ page });
|
||||
await page.getByText('Page Break').click();
|
||||
|
||||
await expect(
|
||||
editor.locator('div[data-content-type="pageBreak"]'),
|
||||
).toBeVisible();
|
||||
|
||||
await writeInEditor({ page, text: 'World' });
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
});
|
||||
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfParse = new PDFParse({ data: pdfBuffer });
|
||||
const pdfInfo = await pdfParse.getInfo();
|
||||
const pdfText = await pdfParse.getText();
|
||||
|
||||
expect(pdfInfo.total).toBe(2);
|
||||
expect(pdfText.pages).toStrictEqual([
|
||||
{ text: 'Hello', num: 1 },
|
||||
{ text: 'World', num: 2 },
|
||||
]);
|
||||
expect(pdfInfo?.info.Title).toBe(randomDoc);
|
||||
});
|
||||
|
||||
/**
|
||||
* We override the document content to ensure that the exported DOCX
|
||||
* contains various elements for testing.
|
||||
* We don't check the content of the DOCX here, just that the export works
|
||||
* and the file is correctly named.
|
||||
*/
|
||||
test('it exports the doc to docx', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Resizable image with caption').click();
|
||||
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.getByText('Upload image').click();
|
||||
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
|
||||
|
||||
const image = page
|
||||
.locator('.--docs--editor-container img.bn-visual-media')
|
||||
.first();
|
||||
|
||||
await expect(image).toBeVisible();
|
||||
const randomDoc = await overrideDocContent({ page, browserName });
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
@@ -143,29 +77,14 @@ test.describe('Doc Export', () => {
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.docx`);
|
||||
});
|
||||
|
||||
/**
|
||||
* We override the document content to ensure that the exported ODT
|
||||
* contains various elements for testing.
|
||||
* We don't check the content of the ODT here, just that the export works
|
||||
* and the file is correctly named.
|
||||
*/
|
||||
test('it exports the doc to odt', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-editor-odt', browserName, 1);
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World ODT');
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Resizable image with caption').click();
|
||||
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.getByText('Upload image').click();
|
||||
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
|
||||
|
||||
const image = page
|
||||
.locator('.--docs--editor-container img.bn-visual-media')
|
||||
.first();
|
||||
|
||||
await expect(image).toBeVisible();
|
||||
const randomDoc = await overrideDocContent({ page, browserName });
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
@@ -358,108 +277,6 @@ test.describe('Doc Export', () => {
|
||||
expect(pdfText.text).toContain('Hello World');
|
||||
});
|
||||
|
||||
test('it exports the doc with quotes', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'export-quotes', browserName, 1);
|
||||
|
||||
const editor = page.locator('.ProseMirror.bn-editor');
|
||||
// Trigger slash menu to show menu
|
||||
await editor.click();
|
||||
await editor.fill('/');
|
||||
await page.getByText('Quote or excerpt').click();
|
||||
|
||||
await expect(
|
||||
editor.locator('.bn-block-content[data-content-type="quote"]'),
|
||||
).toBeVisible();
|
||||
|
||||
await editor
|
||||
.locator('.bn-block-content[data-content-type="quote"]')
|
||||
.fill('Hello World');
|
||||
|
||||
await expect(editor.getByText('Hello World')).toHaveCSS(
|
||||
'font-style',
|
||||
'italic',
|
||||
);
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
});
|
||||
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfParse = new PDFParse({ data: pdfBuffer });
|
||||
const pdfText = await pdfParse.getText();
|
||||
expect(pdfText.text).toContain('Hello World');
|
||||
});
|
||||
|
||||
test('it exports the doc with multi columns', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [randomDoc] = await createDoc(
|
||||
page,
|
||||
'doc-multi-columns',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
|
||||
await page.getByText('Three Columns', { exact: true }).click();
|
||||
|
||||
await page.locator('.bn-block-column').first().fill('Column 1');
|
||||
await page.locator('.bn-block-column').nth(1).fill('Column 2');
|
||||
await page.locator('.bn-block-column').last().fill('Column 3');
|
||||
|
||||
expect(await page.locator('.bn-block-column').count()).toBe(3);
|
||||
await expect(
|
||||
page.locator('.bn-block-column[data-node-type="column"]').first(),
|
||||
).toHaveText('Column 1');
|
||||
await expect(
|
||||
page.locator('.bn-block-column[data-node-type="column"]').nth(1),
|
||||
).toHaveText('Column 2');
|
||||
await expect(
|
||||
page.locator('.bn-block-column[data-node-type="column"]').last(),
|
||||
).toHaveText('Column 3');
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByTestId('doc-open-modal-download-button'),
|
||||
).toBeVisible();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
});
|
||||
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfParse = new PDFParse({ data: pdfBuffer });
|
||||
const pdfText = await pdfParse.getText();
|
||||
expect(pdfText.text).toContain('Column 1');
|
||||
expect(pdfText.text).toContain('Column 2');
|
||||
expect(pdfText.text).toContain('Column 3');
|
||||
});
|
||||
|
||||
test('it injects the correct language attribute into PDF export', async ({
|
||||
page,
|
||||
browserName,
|
||||
@@ -506,54 +323,11 @@ test.describe('Doc Export', () => {
|
||||
expect(pdfString).toContain('/Lang (fr)');
|
||||
});
|
||||
|
||||
test('it exports the doc with interlinking', async ({
|
||||
test('it exports the doc to PDF and checks regressions', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [randomDoc] = await createDoc(
|
||||
page,
|
||||
'export-interlinking',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
const { name: docChild } = await createRootSubPage(
|
||||
page,
|
||||
browserName,
|
||||
'export-interlink-child',
|
||||
);
|
||||
|
||||
await verifyDocName(page, docChild);
|
||||
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Link a doc').first().click();
|
||||
|
||||
const input = page.locator(
|
||||
"span[data-inline-content-type='interlinkingSearchInline'] input",
|
||||
);
|
||||
const searchContainer = page.locator('.quick-search-container');
|
||||
|
||||
await input.fill('export-interlink');
|
||||
|
||||
await expect(searchContainer).toBeVisible();
|
||||
await expect(searchContainer.getByText(randomDoc)).toBeVisible();
|
||||
|
||||
// We are in docChild, we want to create a link to randomDoc (parent)
|
||||
await searchContainer.getByText(randomDoc).click();
|
||||
|
||||
// Search the interlinking link in the editor (not in the document tree)
|
||||
const editor = page.locator('.ProseMirror.bn-editor');
|
||||
const interlink = editor.getByRole('button', {
|
||||
name: randomDoc,
|
||||
});
|
||||
|
||||
await expect(interlink).toBeVisible();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${docChild}.pdf`);
|
||||
});
|
||||
const randomDoc = await overrideDocContent({ page, browserName });
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
@@ -561,78 +335,155 @@ test.describe('Doc Export', () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${docChild}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfParse = new PDFParse({ data: pdfBuffer });
|
||||
const pdfText = await pdfParse.getText();
|
||||
expect(pdfText.text).toContain(randomDoc);
|
||||
});
|
||||
|
||||
test('it exports the doc with interlinking to odt', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [randomDoc] = await createDoc(
|
||||
page,
|
||||
'export-interlinking-odt',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
const { name: docChild } = await createRootSubPage(
|
||||
page,
|
||||
browserName,
|
||||
'export-interlink-child-odt',
|
||||
);
|
||||
|
||||
await verifyDocName(page, docChild);
|
||||
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Link a doc').first().click();
|
||||
|
||||
const input = page.locator(
|
||||
"span[data-inline-content-type='interlinkingSearchInline'] input",
|
||||
);
|
||||
const searchContainer = page.locator('.quick-search-container');
|
||||
|
||||
await input.fill('export-interlink');
|
||||
|
||||
await expect(searchContainer).toBeVisible();
|
||||
await expect(searchContainer.getByText(randomDoc)).toBeVisible();
|
||||
|
||||
// We are in docChild, we want to create a link to randomDoc (parent)
|
||||
await searchContainer.getByText(randomDoc).click();
|
||||
|
||||
// Search the interlinking link in the editor (not in the document tree)
|
||||
const editor = page.locator('.ProseMirror.bn-editor');
|
||||
const interlink = editor.getByRole('button', {
|
||||
name: randomDoc,
|
||||
});
|
||||
|
||||
await expect(interlink).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.getByRole('combobox', { name: 'Format' }).click();
|
||||
await page.getByRole('option', { name: 'Odt' }).click();
|
||||
await expect(
|
||||
page.getByTestId('doc-open-modal-download-button'),
|
||||
).toBeVisible();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${docChild}.odt`);
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
});
|
||||
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
await page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${docChild}.odt`);
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
|
||||
// If we need to update the PDF regression fixture, uncomment the line below
|
||||
//await savePDFToAssetFolder(download);
|
||||
|
||||
// Assert the generated PDF matches "assets/doc-export-regressions.pdf"
|
||||
await comparePDFWithAssetFolder(download);
|
||||
});
|
||||
});
|
||||
|
||||
export const savePDFToAssetFolder = async (download: Download) => {
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfPath = path.join(__dirname, 'assets', `doc-export-regressions.pdf`);
|
||||
fs.writeFileSync(pdfPath, pdfBuffer);
|
||||
};
|
||||
|
||||
export const comparePDFWithAssetFolder = async (download: Download) => {
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
|
||||
// Load reference PDF for comparison
|
||||
const referencePdfPath = path.join(
|
||||
__dirname,
|
||||
'assets',
|
||||
'doc-export-regressions.pdf',
|
||||
);
|
||||
|
||||
const referencePdfBuffer = fs.readFileSync(referencePdfPath);
|
||||
|
||||
// Parse both PDFs
|
||||
const generatedPdf = new PDFParse({ data: pdfBuffer });
|
||||
const referencePdf = new PDFParse({ data: referencePdfBuffer });
|
||||
|
||||
const [generatedInfo, referenceInfo] = await Promise.all([
|
||||
generatedPdf.getInfo(),
|
||||
referencePdf.getInfo(),
|
||||
]);
|
||||
|
||||
const [generatedScreenshot, referenceScreenshot] = await Promise.all([
|
||||
generatedPdf.getScreenshot(),
|
||||
referencePdf.getScreenshot(),
|
||||
]);
|
||||
generatedScreenshot.pages[0].data;
|
||||
|
||||
const [generatedText, referenceText] = await Promise.all([
|
||||
generatedPdf.getText(),
|
||||
referencePdf.getText(),
|
||||
]);
|
||||
|
||||
// Compare page count
|
||||
expect(generatedInfo.total).toBe(referenceInfo.total);
|
||||
|
||||
// Compare text content
|
||||
expect(generatedText.text).toBe(referenceText.text);
|
||||
|
||||
// Compare screenshots page by page
|
||||
for (let i = 0; i < generatedScreenshot.pages.length; i++) {
|
||||
const genPage = generatedScreenshot.pages[i];
|
||||
const refPage = referenceScreenshot.pages[i];
|
||||
|
||||
expect(genPage.width).toBe(refPage.width);
|
||||
expect(genPage.height).toBe(refPage.height);
|
||||
expect(genPage.data).toStrictEqual(refPage.data);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Override the document content API response to use a test content
|
||||
* This test content contains many blocks to facilitate testing
|
||||
* @param page
|
||||
*/
|
||||
export const overrideDocContent = async ({
|
||||
page,
|
||||
browserName,
|
||||
}: {
|
||||
page: Page;
|
||||
browserName: BrowserName;
|
||||
}) => {
|
||||
// Override content prop with assets/base-content-test-pdf.txt
|
||||
await page.route(/\**\/documents\/\**/, async (route) => {
|
||||
const request = route.request();
|
||||
if (
|
||||
request.method().includes('GET') &&
|
||||
!request.url().includes('page=') &&
|
||||
!request.url().includes('versions') &&
|
||||
!request.url().includes('accesses') &&
|
||||
!request.url().includes('invitations')
|
||||
) {
|
||||
const response = await route.fetch();
|
||||
const json = await response.json();
|
||||
json.content = fs.readFileSync(
|
||||
path.join(__dirname, 'assets/base-content-test-pdf.txt'),
|
||||
'utf-8',
|
||||
);
|
||||
void route.fulfill({
|
||||
response,
|
||||
body: JSON.stringify(json),
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
const [randomDoc] = await createDoc(
|
||||
page,
|
||||
'doc-export-override-content',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
// Add Image SVG
|
||||
await page.keyboard.press('Enter');
|
||||
const { suggestionMenu } = await openSuggestionMenu({ page });
|
||||
await suggestionMenu.getByText('Resizable image with caption').click();
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.getByText('Upload image').click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
|
||||
const image = page
|
||||
.locator('.--docs--editor-container img.bn-visual-media[src$=".svg"]')
|
||||
.first();
|
||||
await expect(image).toBeVisible();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// Add Image PNG
|
||||
await openSuggestionMenu({ page });
|
||||
await suggestionMenu.getByText('Resizable image with caption').click();
|
||||
const fileChooserPNGPromise = page.waitForEvent('filechooser');
|
||||
await page.getByText('Upload image').click();
|
||||
const fileChooserPNG = await fileChooserPNGPromise;
|
||||
await fileChooserPNG.setFiles(
|
||||
path.join(__dirname, 'assets/logo-suite-numerique.png'),
|
||||
);
|
||||
const imagePng = page
|
||||
.locator('.--docs--editor-container img.bn-visual-media[src$=".png"]')
|
||||
.first();
|
||||
await expect(imagePng).toBeVisible();
|
||||
|
||||
return randomDoc;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { Page, expect, test } from '@playwright/test';
|
||||
|
||||
import { getEditor } from './utils-editor';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Doc Import', () => {
|
||||
test('it imports 2 docs with the import icon', async ({ page }) => {
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.getByLabel('Open the upload dialog').click();
|
||||
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(path.join(__dirname, 'assets/test_import.docx'));
|
||||
await fileChooser.setFiles(path.join(__dirname, 'assets/test_import.md'));
|
||||
|
||||
await expect(
|
||||
page.getByText(
|
||||
'The document "test_import.docx" has been successfully imported',
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(
|
||||
'The document "test_import.md" has been successfully imported',
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid.getByText('test_import.docx').first()).toBeVisible();
|
||||
await expect(docsGrid.getByText('test_import.md').first()).toBeVisible();
|
||||
|
||||
// Check content of imported md
|
||||
await docsGrid.getByText('test_import.md').first().click();
|
||||
const editor = await getEditor({ page });
|
||||
|
||||
const contentCheck = async (isMDCheck = false) => {
|
||||
await expect(
|
||||
editor.getByRole('heading', {
|
||||
name: 'Lorem Ipsum import Document',
|
||||
level: 1,
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
editor.getByRole('heading', {
|
||||
name: 'Introduction',
|
||||
level: 2,
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
editor.getByRole('heading', {
|
||||
name: 'Subsection 1.1',
|
||||
level: 3,
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
editor
|
||||
.locator('div[data-content-type="bulletListItem"] strong')
|
||||
.getByText('Bold text'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
editor
|
||||
.locator('div[data-content-type="codeBlock"]')
|
||||
.getByText('hello_world'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
editor
|
||||
.locator('div[data-content-type="table"] td')
|
||||
.getByText('Paragraph'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
editor.locator('a[href="http://localhost:3000/"]').getByText('Example'),
|
||||
).toBeVisible();
|
||||
|
||||
/* eslint-disable playwright/no-conditional-expect */
|
||||
if (isMDCheck) {
|
||||
await expect(
|
||||
editor.locator(
|
||||
'img[src="http://localhost:3000/assets/logo-suite-numerique.png"]',
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
editor.locator(
|
||||
'img[src="http://localhost:3000/assets/icon-docs.svg"]',
|
||||
),
|
||||
).toBeVisible();
|
||||
} else {
|
||||
await expect(editor.locator('img')).toHaveCount(2);
|
||||
}
|
||||
/* eslint-enable playwright/no-conditional-expect */
|
||||
|
||||
await expect(
|
||||
editor.locator('div[data-content-type="divider"] hr'),
|
||||
).toBeVisible();
|
||||
};
|
||||
|
||||
await contentCheck();
|
||||
|
||||
// Check content of imported docx
|
||||
await page.getByLabel('Back to homepage').first().click();
|
||||
await docsGrid.getByText('test_import.docx').first().click();
|
||||
|
||||
await contentCheck();
|
||||
});
|
||||
|
||||
test('it imports 2 docs with the drag and drop area', async ({ page }) => {
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid).toBeVisible();
|
||||
|
||||
await dragAndDropFiles(page, "[data-testid='docs-grid']", [
|
||||
{
|
||||
filePath: path.join(__dirname, 'assets/test_import.docx'),
|
||||
fileName: 'test_import.docx',
|
||||
fileType:
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
},
|
||||
{
|
||||
filePath: path.join(__dirname, 'assets/test_import.md'),
|
||||
fileName: 'test_import.md',
|
||||
fileType: 'text/markdown',
|
||||
},
|
||||
]);
|
||||
|
||||
// Wait for success messages
|
||||
await expect(
|
||||
page.getByText(
|
||||
'The document "test_import.docx" has been successfully imported',
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(
|
||||
'The document "test_import.md" has been successfully imported',
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(docsGrid.getByText('test_import.docx').first()).toBeVisible();
|
||||
await expect(docsGrid.getByText('test_import.md').first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
const dragAndDropFiles = async (
|
||||
page: Page,
|
||||
selector: string,
|
||||
files: Array<{ filePath: string; fileName: string; fileType?: string }>,
|
||||
) => {
|
||||
const filesData = files.map((file) => ({
|
||||
bufferData: `data:application/octet-stream;base64,${readFileSync(file.filePath).toString('base64')}`,
|
||||
fileName: file.fileName,
|
||||
fileType: file.fileType || '',
|
||||
}));
|
||||
|
||||
const dataTransfer = await page.evaluateHandle(async (filesInfo) => {
|
||||
const dt = new DataTransfer();
|
||||
|
||||
for (const fileInfo of filesInfo) {
|
||||
const blobData = await fetch(fileInfo.bufferData).then((res) =>
|
||||
res.blob(),
|
||||
);
|
||||
const file = new File([blobData], fileInfo.fileName, {
|
||||
type: fileInfo.fileType,
|
||||
});
|
||||
dt.items.add(file);
|
||||
}
|
||||
|
||||
return dt;
|
||||
}, filesData);
|
||||
|
||||
await page.dispatchEvent(selector, 'drop', { dataTransfer });
|
||||
};
|
||||
@@ -42,8 +42,8 @@ test.describe('Doc Version', () => {
|
||||
// Write more
|
||||
await writeInEditor({ page, text: 'It will create a version' });
|
||||
|
||||
await openSuggestionMenu({ page });
|
||||
await page.getByText('Add a callout block').click();
|
||||
const { suggestionMenu } = await openSuggestionMenu({ page });
|
||||
await suggestionMenu.getByText('Add a callout block').click();
|
||||
|
||||
const calloutBlock = page
|
||||
.locator('div[data-content-type="callout"]')
|
||||
|
||||
@@ -109,8 +109,10 @@ test.describe('Language', () => {
|
||||
}) => {
|
||||
await createDoc(page, 'doc-toolbar', browserName, 1);
|
||||
|
||||
const editor = await openSuggestionMenu({ page });
|
||||
await expect(page.getByText('Headings', { exact: true })).toBeVisible();
|
||||
const { editor, suggestionMenu } = await openSuggestionMenu({ page });
|
||||
await expect(
|
||||
suggestionMenu.getByText('Headings', { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await editor.click(); // close the menu
|
||||
|
||||
@@ -121,6 +123,8 @@ test.describe('Language', () => {
|
||||
|
||||
// Trigger slash menu to show french menu
|
||||
await openSuggestionMenu({ page });
|
||||
await expect(page.getByText('Titres', { exact: true })).toBeVisible();
|
||||
await expect(
|
||||
suggestionMenu.getByText('Titres', { exact: true }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const CONFIG = {
|
||||
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY: true,
|
||||
ENVIRONMENT: 'development',
|
||||
FRONTEND_CSS_URL: null,
|
||||
FRONTEND_JS_URL: null,
|
||||
FRONTEND_HOMEPAGE_FEATURE_ENABLED: true,
|
||||
FRONTEND_THEME: null,
|
||||
MEDIA_BASE_URL: 'http://localhost:8083',
|
||||
|
||||
@@ -11,7 +11,9 @@ export const openSuggestionMenu = async ({ page }: { page: Page }) => {
|
||||
await editor.click();
|
||||
await writeInEditor({ page, text: '/' });
|
||||
|
||||
return editor;
|
||||
const suggestionMenu = page.locator('.bn-suggestion-menu');
|
||||
|
||||
return { editor, suggestionMenu };
|
||||
};
|
||||
|
||||
export const writeInEditor = async ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"author": "DINUM",
|
||||
"license": "MIT",
|
||||
@@ -15,7 +15,7 @@
|
||||
"test:ui::chromium": "yarn test:ui --project=chromium"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.56.1",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@types/node": "*",
|
||||
"@types/pdf-parse": "1.1.5",
|
||||
"eslint-plugin-docs": "*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"author": "DINUM",
|
||||
"license": "MIT",
|
||||
@@ -19,14 +19,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ag-media/react-pdf-table": "2.0.3",
|
||||
"@blocknote/code-block": "0.44.2",
|
||||
"@blocknote/core": "0.44.2",
|
||||
"@blocknote/mantine": "0.44.2",
|
||||
"@blocknote/react": "0.44.2",
|
||||
"@blocknote/xl-docx-exporter": "0.44.2",
|
||||
"@blocknote/xl-multi-column": "0.44.2",
|
||||
"@blocknote/xl-odt-exporter": "0.44.2",
|
||||
"@blocknote/xl-pdf-exporter": "0.44.2",
|
||||
"@blocknote/code-block": "0.45.0",
|
||||
"@blocknote/core": "0.45.0",
|
||||
"@blocknote/mantine": "0.45.0",
|
||||
"@blocknote/react": "0.45.0",
|
||||
"@blocknote/xl-docx-exporter": "0.45.0",
|
||||
"@blocknote/xl-multi-column": "0.45.0",
|
||||
"@blocknote/xl-odt-exporter": "0.45.0",
|
||||
"@blocknote/xl-pdf-exporter": "0.45.0",
|
||||
"@dnd-kit/core": "6.3.1",
|
||||
"@dnd-kit/modifiers": "9.0.0",
|
||||
"@emoji-mart/data": "1.2.1",
|
||||
@@ -35,14 +35,14 @@
|
||||
"@fontsource-variable/material-symbols-outlined": "5.2.30",
|
||||
"@fontsource/material-icons": "5.2.7",
|
||||
"@gouvfr-lasuite/integration": "1.0.3",
|
||||
"@gouvfr-lasuite/ui-kit": "0.18.0",
|
||||
"@hocuspocus/provider": "3.4.0",
|
||||
"@mantine/core": "8.3.9",
|
||||
"@mantine/hooks": "8.3.9",
|
||||
"@gouvfr-lasuite/ui-kit": "0.18.4",
|
||||
"@hocuspocus/provider": "3.4.3",
|
||||
"@mantine/core": "8.3.10",
|
||||
"@mantine/hooks": "8.3.10",
|
||||
"@openfun/cunningham-react": "4.0.0",
|
||||
"@react-pdf/renderer": "4.3.1",
|
||||
"@sentry/nextjs": "10.27.0",
|
||||
"@tanstack/react-query": "5.90.10",
|
||||
"@sentry/nextjs": "10.30.0",
|
||||
"@tanstack/react-query": "5.90.12",
|
||||
"@tiptap/extensions": "*",
|
||||
"canvg": "4.0.3",
|
||||
"clsx": "2.1.1",
|
||||
@@ -52,17 +52,18 @@
|
||||
"emoji-datasource-apple": "16.0.0",
|
||||
"emoji-mart": "5.6.0",
|
||||
"emoji-regex": "10.6.0",
|
||||
"i18next": "25.6.3",
|
||||
"i18next": "25.7.2",
|
||||
"i18next-browser-languagedetector": "8.2.0",
|
||||
"idb": "8.0.3",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.7.2",
|
||||
"next": "15.5.9",
|
||||
"posthog-js": "1.298.0",
|
||||
"posthog-js": "1.306.1",
|
||||
"react": "*",
|
||||
"react-aria-components": "1.13.0",
|
||||
"react-dom": "*",
|
||||
"react-i18next": "16.3.5",
|
||||
"react-dropzone": "14.3.8",
|
||||
"react-i18next": "16.5.0",
|
||||
"react-intersection-observer": "10.0.0",
|
||||
"react-resizable-panels": "3.0.6",
|
||||
"react-select": "5.10.2",
|
||||
@@ -70,11 +71,11 @@
|
||||
"use-debounce": "10.0.6",
|
||||
"y-protocols": "1.0.6",
|
||||
"yjs": "*",
|
||||
"zustand": "5.0.8"
|
||||
"zustand": "5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@tanstack/react-query-devtools": "5.91.0",
|
||||
"@tanstack/react-query-devtools": "5.91.1",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/jest-dom": "6.9.1",
|
||||
"@testing-library/react": "16.3.0",
|
||||
@@ -84,21 +85,21 @@
|
||||
"@types/node": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"@vitejs/plugin-react": "5.1.1",
|
||||
"@vitejs/plugin-react": "5.1.2",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"cross-env": "10.1.0",
|
||||
"dotenv": "17.2.3",
|
||||
"eslint-plugin-docs": "*",
|
||||
"fetch-mock": "9.11.0",
|
||||
"jsdom": "27.2.0",
|
||||
"jsdom": "27.3.0",
|
||||
"node-fetch": "2.7.0",
|
||||
"prettier": "3.6.2",
|
||||
"stylelint": "16.26.0",
|
||||
"prettier": "3.7.4",
|
||||
"stylelint": "16.26.1",
|
||||
"stylelint-config-standard": "39.0.1",
|
||||
"stylelint-prettier": "5.0.3",
|
||||
"typescript": "*",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "4.0.13",
|
||||
"vite-tsconfig-paths": "6.0.1",
|
||||
"vitest": "4.0.15",
|
||||
"webpack": "5.103.0",
|
||||
"workbox-webpack-plugin": "7.1.0"
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@ export type DefinedInitialDataInfiniteOptionsAPI<
|
||||
QueryKey,
|
||||
TPageParam
|
||||
>;
|
||||
|
||||
export type UseInfiniteQueryResultAPI<Q> = InfiniteData<Q>;
|
||||
export type InfiniteQueryConfig<Q> = Omit<
|
||||
DefinedInitialDataInfiniteOptionsAPI<Q>,
|
||||
'queryKey' | 'initialData' | 'getNextPageParam' | 'initialPageParam'
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6.12757 9.8486C5.98657 9.6993 5.91709 9.5143 5.91709 9.30858C5.91709 9.10284 5.98679 8.91775 6.13233 8.77221C6.28262 8.62192 6.47291 8.54842 6.68579 8.54842H13.1697C13.3775 8.54842 13.5623 8.62245 13.7061 8.77215C13.8559 8.91601 13.9299 9.10081 13.9299 9.30858C13.9299 9.51737 13.8553 9.70306 13.7085 9.8511C13.5643 10.0024 13.3787 10.0773 13.1697 10.0773H6.68579C6.47291 10.0773 6.28262 10.0038 6.13233 9.85349L6.13076 9.85192L6.12757 9.8486Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M6.12757 12.83C5.98657 12.6807 5.91709 12.4957 5.91709 12.29C5.91709 12.0843 5.98679 11.8992 6.13233 11.7536C6.28262 11.6033 6.47291 11.5298 6.68579 11.5298H13.1697C13.3775 11.5298 13.5623 11.6039 13.7061 11.7536C13.8559 11.8974 13.9299 12.0822 13.9299 12.29C13.9299 12.4988 13.8553 12.6845 13.7085 12.8325C13.5643 12.9838 13.3787 13.0587 13.1697 13.0587H6.68579C6.47291 13.0587 6.28262 12.9852 6.13233 12.8349L6.13076 12.8333L6.12757 12.83Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M5.91709 15.2885C5.91709 15.4912 5.98839 15.6726 6.12757 15.82L6.134 15.8266L6.13723 15.8296C6.28833 15.9723 6.47704 16.0401 6.68579 16.0401H9.75263C9.96123 16.0401 10.1502 15.9722 10.2975 15.8249C10.444 15.6784 10.5213 15.4956 10.5213 15.2885C10.5213 15.0768 10.4486 14.8874 10.2999 14.7374C10.1539 14.5842 9.96433 14.5113 9.75263 14.5113H6.68579C6.47293 14.5113 6.28257 14.5847 6.13226 14.735L6.12757 14.7399C5.98486 14.891 5.91709 15.0797 5.91709 15.2885Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.37975 1.24597C7.88425 0.735004 8.61944 0.5 9.54031 0.5H18.6127C19.533 0.5 20.2661 0.734736 20.7653 1.24652C21.2686 1.75666 21.5 2.49628 21.5 3.42147V16.3808C21.5 17.3112 21.2688 18.0521 20.7638 18.5572C20.2645 19.0624 19.532 19.2937 18.6127 19.2937H17.347V20.5338C17.347 21.4641 17.1158 22.2051 16.6108 22.7102C16.1115 23.2153 15.3789 23.4467 14.4597 23.4467H5.3873C4.46721 23.4467 3.73242 23.2149 3.22781 22.7103C2.72908 22.2051 2.5 21.4635 2.5 20.5338V7.57442C2.5 6.64962 2.72942 5.90915 3.22673 5.39893C3.73123 4.88796 4.46643 4.65295 5.3873 4.65295H6.65302V3.42147C6.65302 2.49666 6.88244 1.7562 7.37975 1.24597ZM8.42319 4.65295H14.4597C15.38 4.65295 16.1131 4.88769 16.6122 5.39947C17.1156 5.90962 17.347 6.64923 17.347 7.57442V17.5236H18.5444C18.9636 17.5236 19.2496 17.4163 19.4324 17.2289L19.4337 17.2275C19.6238 17.0374 19.7298 16.7549 19.7298 16.3552V3.4471C19.7298 3.04734 19.6238 2.76485 19.4337 2.57481L19.431 2.57206C19.248 2.37972 18.9625 2.27017 18.5444 2.27017H9.60866C9.19081 2.27017 8.90126 2.37956 8.71212 2.57341C8.52701 2.76329 8.42319 3.04633 8.42319 3.4471V4.65295ZM5.45564 21.6765C5.03728 21.6765 4.74743 21.5697 4.55844 21.3811C4.37372 21.1913 4.27017 20.9084 4.27017 20.5081V7.60005C4.27017 7.19928 4.37399 6.91625 4.55911 6.72636C4.74825 6.53252 5.03779 6.42313 5.45564 6.42313H14.3913C14.8095 6.42313 15.095 6.53268 15.278 6.72501L15.2807 6.72776C15.4708 6.9178 15.5768 7.20029 15.5768 7.60005V20.5081C15.5768 20.9079 15.4708 21.1904 15.2807 21.3804L15.2793 21.3818C15.0966 21.5693 14.8105 21.6765 14.3913 21.6765H5.45564Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -1,21 +1,34 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Text, TextType } from '@/components';
|
||||
|
||||
type IconProps = TextType & {
|
||||
type IconBase = TextType & {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type IconMaterialProps = IconBase & {
|
||||
iconName: string;
|
||||
variant?: 'filled' | 'outlined' | 'symbols-outlined';
|
||||
icon?: never;
|
||||
};
|
||||
|
||||
type IconSVGProps = IconBase & {
|
||||
icon: React.ReactNode;
|
||||
iconName?: never;
|
||||
variant?: never;
|
||||
};
|
||||
|
||||
export const Icon = ({
|
||||
className,
|
||||
iconName,
|
||||
disabled,
|
||||
iconName,
|
||||
icon,
|
||||
variant = 'outlined',
|
||||
$theme = 'neutral',
|
||||
...textProps
|
||||
}: IconProps) => {
|
||||
}: IconMaterialProps | IconSVGProps) => {
|
||||
const hasLabel = 'aria-label' in textProps || 'aria-labelledby' in textProps;
|
||||
const ariaHidden =
|
||||
'aria-hidden' in textProps ? textProps['aria-hidden'] : !hasLabel;
|
||||
@@ -24,15 +37,15 @@ export const Icon = ({
|
||||
<Text
|
||||
aria-hidden={ariaHidden}
|
||||
className={clsx('--docs--icon-bg', className, {
|
||||
'material-icons-filled': variant === 'filled',
|
||||
'material-icons': variant === 'outlined',
|
||||
'material-symbols-outlined': variant === 'symbols-outlined',
|
||||
'material-icons-filled': variant === 'filled' && iconName,
|
||||
'material-icons': variant === 'outlined' && iconName,
|
||||
'material-symbols-outlined': variant === 'symbols-outlined' && iconName,
|
||||
})}
|
||||
$theme={disabled ? 'disabled' : $theme}
|
||||
aria-disabled={disabled}
|
||||
{...textProps}
|
||||
>
|
||||
{iconName}
|
||||
{iconName ?? icon}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import Head from 'next/head';
|
||||
import Script from 'next/script';
|
||||
import { PropsWithChildren, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -87,6 +88,9 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
|
||||
<link rel="stylesheet" href={conf?.FRONTEND_CSS_URL} />
|
||||
</Head>
|
||||
)}
|
||||
{conf?.FRONTEND_JS_URL && (
|
||||
<Script src={conf?.FRONTEND_JS_URL} strategy="afterInteractive" />
|
||||
)}
|
||||
<AnalyticsProvider>
|
||||
<CrispProvider websiteId={conf?.CRISP_WEBSITE_ID}>
|
||||
{children}
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface ConfigResponse {
|
||||
ENVIRONMENT: string;
|
||||
FRONTEND_CSS_URL?: string;
|
||||
FRONTEND_HOMEPAGE_FEATURE_ENABLED?: boolean;
|
||||
FRONTEND_JS_URL?: string;
|
||||
FRONTEND_THEME?: Theme;
|
||||
LANGUAGES: [string, string][];
|
||||
LANGUAGE_CODE: string;
|
||||
|
||||
@@ -44,7 +44,7 @@ export const Auth = ({ children }: PropsWithChildren) => {
|
||||
if (config?.FRONTEND_HOMEPAGE_FEATURE_ENABLED) {
|
||||
if (pathname !== HOME_URL) {
|
||||
setIsRedirecting(true);
|
||||
void replace(HOME_URL).then(() => setIsRedirecting(false));
|
||||
window.location.replace(HOME_URL);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { baseApiUrl } from '@/api';
|
||||
|
||||
export const HOME_URL = '/home';
|
||||
export const HOME_URL = '/home/';
|
||||
export const LOGIN_URL = `${baseApiUrl()}authenticate/`;
|
||||
export const LOGOUT_URL = `${baseApiUrl()}logout/`;
|
||||
export const PATH_AUTH_LOCAL_STORAGE = 'docs-path-auth';
|
||||
|
||||
+7
-6
@@ -16,13 +16,13 @@ import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
import type { Awareness } from 'y-protocols/awareness';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { Box, TextErrors } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, useProviderStore } from '@/docs/doc-management';
|
||||
import { avatarUrlFromName, useAuth } from '@/features/auth';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import {
|
||||
useHeadings,
|
||||
@@ -85,12 +85,11 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
const { setEditor } = useEditorStore();
|
||||
const { t } = useTranslation();
|
||||
const { themeTokens } = useCunninghamTheme();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { isSynced: isConnectedToCollabServer } = useProviderStore();
|
||||
const refEditorContainer = useRef<HTMLDivElement>(null);
|
||||
const canSeeComment = doc.abilities.comment;
|
||||
// Determine if comments should be visible in the UI
|
||||
const showComments = canSeeComment && isDesktop;
|
||||
const showComments = canSeeComment;
|
||||
|
||||
useSaveDoc(doc.id, provider.document, isConnectedToCollabServer);
|
||||
const { i18n } = useTranslation();
|
||||
@@ -117,7 +116,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
const editor: DocsBlockNoteEditor = useCreateBlockNote(
|
||||
{
|
||||
collaboration: {
|
||||
provider: provider,
|
||||
provider: provider as { awareness?: Awareness | undefined },
|
||||
fragment: provider.document.getXmlFragment('document-store'),
|
||||
user: {
|
||||
name: cursorName,
|
||||
@@ -163,8 +162,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
},
|
||||
dictionary: {
|
||||
...locales[lang as keyof typeof locales],
|
||||
multi_column:
|
||||
multiColumnLocales?.[lang as keyof typeof multiColumnLocales],
|
||||
...(multiColumnLocales && {
|
||||
multi_column:
|
||||
multiColumnLocales[lang as keyof typeof multiColumnLocales],
|
||||
}),
|
||||
},
|
||||
pasteHandler: ({ event, defaultPasteHandler }) => {
|
||||
// Get clipboard data
|
||||
|
||||
-3
@@ -18,7 +18,6 @@ import { css } from 'styled-components';
|
||||
import { Box, Icon } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useDocStore } from '@/features/docs/doc-management';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import {
|
||||
DocsBlockSchema,
|
||||
@@ -31,7 +30,6 @@ export const CommentToolbarButton = () => {
|
||||
const { currentDoc } = useDocStore();
|
||||
const { t } = useTranslation();
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const comments = useExtension('comments') as unknown as ReturnType<
|
||||
ReturnType<typeof CommentsExtension>
|
||||
>;
|
||||
@@ -61,7 +59,6 @@ export const CommentToolbarButton = () => {
|
||||
|
||||
if (
|
||||
!comments ||
|
||||
!isDesktop ||
|
||||
!show ||
|
||||
!editor.isEditable ||
|
||||
!Components ||
|
||||
|
||||
+29
-6
@@ -37,7 +37,8 @@ export class DocsThreadStore extends ThreadStore {
|
||||
if (docAuth.canSee) {
|
||||
this.awareness = awareness;
|
||||
this.awareness?.on('update', this.onAwarenessUpdate);
|
||||
void this.refreshThreads();
|
||||
|
||||
this.refreshThreads();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +99,9 @@ export class DocsThreadStore extends ThreadStore {
|
||||
|
||||
// If we know the threadId, schedule a targeted refresh. Otherwise, fall back to full refresh.
|
||||
if (ping.threadId) {
|
||||
await this.refreshThread(ping.threadId);
|
||||
void this.refreshThread(ping.threadId);
|
||||
} else {
|
||||
await this.refreshThreads();
|
||||
this.refreshThreads();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -211,6 +212,17 @@ export class DocsThreadStore extends ThreadStore {
|
||||
.setMark?.('comment', { orphan: false, threadId })
|
||||
.run?.();
|
||||
|
||||
/**
|
||||
* We have some issues with mobiles and the formatting toolbar reopening
|
||||
* after adding a comment, so we restore the cursor position here.
|
||||
* By restoring the cursor position at the head of the selection,
|
||||
* it will automatically close the formatting toolbar.
|
||||
*/
|
||||
const cursorPos = editor._tiptapEditor?.state.selection.head;
|
||||
if (cursorPos !== undefined) {
|
||||
editor._tiptapEditor?.commands.setTextSelection(cursorPos);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
@@ -240,6 +252,7 @@ export class DocsThreadStore extends ThreadStore {
|
||||
this.upsertClientThreadData(threadData);
|
||||
this.notifySubscribers();
|
||||
this.ping(threadData.id);
|
||||
|
||||
return threadData;
|
||||
};
|
||||
|
||||
@@ -280,7 +293,7 @@ export class DocsThreadStore extends ThreadStore {
|
||||
}),
|
||||
);
|
||||
|
||||
await this.refreshThreads();
|
||||
this.refreshThreads();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -298,7 +311,17 @@ export class DocsThreadStore extends ThreadStore {
|
||||
this.notifySubscribers();
|
||||
}
|
||||
|
||||
public async refreshThreads(): Promise<void> {
|
||||
public refreshThreads() {
|
||||
this.initThreads().catch((e) => {
|
||||
if (!(e instanceof APIError) || e.status !== 401) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
public async initThreads(): Promise<void> {
|
||||
const response = await fetchAPI(`documents/${this.docId}/threads/`, {
|
||||
method: 'GET',
|
||||
});
|
||||
@@ -484,7 +507,7 @@ export class DocsThreadStore extends ThreadStore {
|
||||
);
|
||||
}
|
||||
|
||||
await this.refreshThreads();
|
||||
this.refreshThreads();
|
||||
this.ping(threadId);
|
||||
};
|
||||
|
||||
|
||||
+5
-2
@@ -26,13 +26,16 @@ export const cssComments = (
|
||||
|
||||
// Thread modal
|
||||
.bn-thread {
|
||||
width: 400px;
|
||||
width: 100%;
|
||||
min-width: calc(min(400px, 90vw));
|
||||
max-width: calc(min(400px, 90vw));
|
||||
max-height: calc(min(500px, 60vh));
|
||||
padding: 8px;
|
||||
box-shadow: 0px 6px 18px 0px #00001229;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
gap: 0;
|
||||
overflow: auto;
|
||||
max-height: 500px;
|
||||
|
||||
.bn-default-styles {
|
||||
font-family: var(--c--globals--font--families--base);
|
||||
|
||||
+68
-15
@@ -13,12 +13,13 @@ import {
|
||||
createReactBlockSpec,
|
||||
} from '@blocknote/react';
|
||||
import { TFunction } from 'i18next';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
import { createGlobalStyle, css } from 'styled-components';
|
||||
|
||||
import { Box, Icon } from '@/components';
|
||||
import { Box, Icon, Loading } from '@/components';
|
||||
|
||||
import { ANALYZE_URL } from '../../conf';
|
||||
import { DocsBlockNoteEditor } from '../../types';
|
||||
|
||||
const PDFBlockStyle = createGlobalStyle`
|
||||
@@ -66,6 +67,9 @@ const PdfBlockComponent = ({
|
||||
const pdfUrl = block.props.url;
|
||||
const { i18n, t } = useTranslation();
|
||||
const lang = i18n.resolvedLanguage;
|
||||
const [isPDFContent, setIsPDFContent] = useState<boolean | null>(null);
|
||||
const [isPDFContentLoading, setIsPDFContentLoading] =
|
||||
useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (lang && locales[lang as keyof typeof locales]) {
|
||||
@@ -82,9 +86,55 @@ const PdfBlockComponent = ({
|
||||
}
|
||||
}, [lang, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pdfUrl || pdfUrl.includes(ANALYZE_URL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validatePDFContent = async () => {
|
||||
setIsPDFContentLoading(true);
|
||||
try {
|
||||
const response = await fetch(pdfUrl, {
|
||||
credentials: 'include',
|
||||
});
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
if (response.ok && contentType?.includes('application/pdf')) {
|
||||
setIsPDFContent(true);
|
||||
} else {
|
||||
setIsPDFContent(false);
|
||||
}
|
||||
} catch {
|
||||
setIsPDFContent(false);
|
||||
} finally {
|
||||
setIsPDFContentLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
void validatePDFContent();
|
||||
}, [pdfUrl]);
|
||||
|
||||
return (
|
||||
<Box ref={contentRef} className="bn-file-block-content-wrapper">
|
||||
<PDFBlockStyle />
|
||||
{isPDFContentLoading && <Loading />}
|
||||
{!isPDFContentLoading && isPDFContent !== null && !isPDFContent && (
|
||||
<Box
|
||||
$align="center"
|
||||
$justify="center"
|
||||
$color="#666"
|
||||
$background="#f5f5f5"
|
||||
$border="1px solid #ddd"
|
||||
$height="300px"
|
||||
$css={css`
|
||||
text-align: center;
|
||||
`}
|
||||
contentEditable={false}
|
||||
onClick={() => editor.setTextCursorPosition(block)}
|
||||
>
|
||||
{t('Invalid or missing PDF file.')}
|
||||
</Box>
|
||||
)}
|
||||
<ResizableFileBlockWrapper
|
||||
buttonIcon={
|
||||
<Icon iconName="upload" $size="24px" $css="line-height: normal;" />
|
||||
@@ -92,18 +142,21 @@ const PdfBlockComponent = ({
|
||||
block={block as unknown as FileBlockBlock}
|
||||
editor={editor as unknown as FileBlockEditor}
|
||||
>
|
||||
<Box
|
||||
className="bn-visual-media"
|
||||
role="presentation"
|
||||
as="embed"
|
||||
$width="100%"
|
||||
$height="450px"
|
||||
type="application/pdf"
|
||||
src={pdfUrl}
|
||||
contentEditable={false}
|
||||
draggable={false}
|
||||
onClick={() => editor.setTextCursorPosition(block)}
|
||||
/>
|
||||
{!isPDFContentLoading && isPDFContent && (
|
||||
<Box
|
||||
as="embed"
|
||||
className="bn-visual-media"
|
||||
role="presentation"
|
||||
$width="100%"
|
||||
$height="450px"
|
||||
type="application/pdf"
|
||||
src={pdfUrl}
|
||||
aria-label={block.props.name || t('PDF document')}
|
||||
contentEditable={false}
|
||||
draggable={false}
|
||||
onClick={() => editor.setTextCursorPosition(block)}
|
||||
/>
|
||||
)}
|
||||
</ResizableFileBlockWrapper>
|
||||
</Box>
|
||||
);
|
||||
|
||||
+6
-2
@@ -79,10 +79,12 @@ const LinkSelected = ({ url, title }: LinkSelectedProps) => {
|
||||
|
||||
return (
|
||||
<BoxButton
|
||||
as="span"
|
||||
className="--docs--interlinking-link-inline-content"
|
||||
onClick={handleClick}
|
||||
draggable="false"
|
||||
$css={css`
|
||||
display: contents;
|
||||
display: inline;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
& svg {
|
||||
@@ -91,7 +93,9 @@ const LinkSelected = ({ url, title }: LinkSelectedProps) => {
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${colorsTokens['gray-100']};
|
||||
background-color: var(
|
||||
--c--contextuals--background--semantic--contextual--primary
|
||||
);
|
||||
}
|
||||
transition: background-color var(--c--globals--transitions--duration)
|
||||
var(--c--globals--transitions--ease-out);
|
||||
|
||||
+1
@@ -171,6 +171,7 @@ export const SearchPage = ({
|
||||
{trigger}
|
||||
<Box
|
||||
as="input"
|
||||
name="doc-search-input"
|
||||
$padding={{ left: '3px' }}
|
||||
$css={inputStyle}
|
||||
ref={inputRef}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const ANALYZE_URL = 'media-check';
|
||||
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { backendUrl } from '@/api';
|
||||
|
||||
import { useCreateDocAttachment } from '../api';
|
||||
import { ANALYZE_URL } from '../conf';
|
||||
import { DocsBlockNoteEditor } from '../types';
|
||||
|
||||
export const useUploadFile = (docId: string) => {
|
||||
@@ -46,7 +47,6 @@ export const useUploadFile = (docId: string) => {
|
||||
* @param editor
|
||||
*/
|
||||
export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
|
||||
const ANALYZE_URL = 'media-check';
|
||||
const { t } = useTranslation();
|
||||
|
||||
/**
|
||||
|
||||
+14
-1
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Derivated from Blockquote PDF mapping
|
||||
* @see: https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/src/pdf/defaultSchema/blocks.tsx
|
||||
*/
|
||||
import { Text } from '@react-pdf/renderer';
|
||||
|
||||
import { DocsExporterPDF } from '../types';
|
||||
@@ -30,6 +34,15 @@ export const blockMappingHeadingPDF: DocsExporterPDF['mappings']['blockMapping']
|
||||
const fontSizeEM =
|
||||
block.props.level === 1 ? 2 : block.props.level === 2 ? 1.5 : 1.17;
|
||||
|
||||
const levelFontSizeEM = {
|
||||
1: 2,
|
||||
2: 1.5,
|
||||
3: 1.17,
|
||||
4: 1,
|
||||
5: 0.83,
|
||||
6: 0.67,
|
||||
}[block.props.level as 1 | 2 | 3 | 4 | 5 | 6];
|
||||
|
||||
// Extract plain text for bookmark title
|
||||
const bookmarkTitle =
|
||||
extractTextFromBlockContent(block.content) || 'Untitled';
|
||||
@@ -42,7 +55,7 @@ export const blockMappingHeadingPDF: DocsExporterPDF['mappings']['blockMapping']
|
||||
title: bookmarkTitle,
|
||||
}}
|
||||
style={{
|
||||
fontSize: fontSizeEM * FONT_SIZE * PIXELS_PER_POINT,
|
||||
fontSize: levelFontSizeEM * FONT_SIZE * PIXELS_PER_POINT,
|
||||
fontWeight: 700,
|
||||
marginTop: `${fontSizeEM * MERGE_RATIO}px`,
|
||||
marginBottom: `${fontSizeEM * MERGE_RATIO}px`,
|
||||
|
||||
@@ -21,9 +21,6 @@ export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['
|
||||
|
||||
if (blob.type.includes('svg')) {
|
||||
const svgText = await blob.text();
|
||||
const FALLBACK_SIZE = 536;
|
||||
previewWidth = previewWidth || FALLBACK_SIZE;
|
||||
|
||||
const result = await convertSvgToPng(svgText, previewWidth);
|
||||
pngConverted = result.png;
|
||||
dimensions = { width: result.width, height: result.height };
|
||||
|
||||
@@ -36,7 +36,7 @@ export function downloadFile(blob: Blob, filename: string) {
|
||||
*/
|
||||
export async function convertSvgToPng(
|
||||
svgText: string,
|
||||
width: number,
|
||||
width?: number,
|
||||
): Promise<{ png: string; width: number; height: number }> {
|
||||
// Create a canvas and render the SVG onto it
|
||||
const canvas = document.createElement('canvas');
|
||||
@@ -54,26 +54,36 @@ export async function convertSvgToPng(
|
||||
const svgElement = svgDoc.documentElement;
|
||||
|
||||
// Get viewBox or fallback to width/height attributes
|
||||
let height;
|
||||
let calculatedHeight: number | undefined;
|
||||
const svgWidth = svgElement.getAttribute?.('width');
|
||||
const svgHeight = svgElement.getAttribute?.('height');
|
||||
const viewBox = svgElement.getAttribute('viewBox')?.split(' ').map(Number);
|
||||
|
||||
const originalWidth = svgWidth ? parseInt(svgWidth) : viewBox?.[2];
|
||||
const originalHeight = svgHeight ? parseInt(svgHeight) : viewBox?.[3];
|
||||
if (originalWidth && originalHeight) {
|
||||
const aspectRatio = originalHeight / originalWidth;
|
||||
height = Math.round(width * aspectRatio);
|
||||
}
|
||||
|
||||
const svg = Canvg.fromString(ctx, svgText);
|
||||
svg.resize(width, height, true);
|
||||
|
||||
const FALLBACK_WIDTH = 536;
|
||||
|
||||
// Resize if width provided, preserving aspect ratio
|
||||
if (originalWidth && originalHeight && width) {
|
||||
const aspectRatio = originalHeight / originalWidth;
|
||||
calculatedHeight = Math.round(width * aspectRatio);
|
||||
svg.resize(width, calculatedHeight, true);
|
||||
} else if (!width && !originalWidth) {
|
||||
svg.resize(FALLBACK_WIDTH, undefined, true);
|
||||
}
|
||||
|
||||
await svg.render();
|
||||
|
||||
const returnWidth = width || originalWidth || FALLBACK_WIDTH;
|
||||
const returnHeight = calculatedHeight || returnWidth;
|
||||
|
||||
return {
|
||||
png: canvas.toDataURL('image/png'),
|
||||
width,
|
||||
height: height || width,
|
||||
width: returnWidth,
|
||||
height: returnHeight,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +172,10 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
||||
emoji={emoji}
|
||||
withEmojiPicker={doc.abilities.partial_update}
|
||||
defaultIcon={
|
||||
<SubPageIcon color="var(--c--contextuals--content--semantic--info--tertiary)" />
|
||||
<SubPageIcon
|
||||
color="var(--c--contextuals--content--semantic--info--tertiary)"
|
||||
style={{ flexShrink: 0 }}
|
||||
/>
|
||||
}
|
||||
$size="sm"
|
||||
docId={doc.id}
|
||||
|
||||
@@ -63,13 +63,11 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
||||
});
|
||||
treeContext?.treeData.handleMove(result);
|
||||
};
|
||||
|
||||
/**
|
||||
* This function resets the tree states.
|
||||
*/
|
||||
const resetStateTree = useCallback(() => {
|
||||
if (!treeContext?.root?.id) {
|
||||
return;
|
||||
}
|
||||
treeContext?.setRoot(null);
|
||||
setInitialOpenState(undefined);
|
||||
}, [treeContext]);
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
||||
import {
|
||||
UseMutationOptions,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
APIError,
|
||||
UseInfiniteQueryResultAPI,
|
||||
errorCauses,
|
||||
fetchAPI,
|
||||
} from '@/api';
|
||||
import { Doc, DocsResponse, KEY_LIST_DOC } from '@/docs/doc-management';
|
||||
|
||||
enum ContentTypes {
|
||||
Docx = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
Markdown = 'text/markdown',
|
||||
OctetStream = 'application/octet-stream',
|
||||
}
|
||||
|
||||
export enum ContentTypesAllowed {
|
||||
Docx = ContentTypes.Docx,
|
||||
Markdown = ContentTypes.Markdown,
|
||||
}
|
||||
|
||||
const getMimeType = (file: File): string => {
|
||||
if (file.type) {
|
||||
return file.type;
|
||||
}
|
||||
|
||||
const extension = file.name.split('.').pop()?.toLowerCase();
|
||||
|
||||
switch (extension) {
|
||||
case 'md':
|
||||
return ContentTypes.Markdown;
|
||||
case 'markdown':
|
||||
return ContentTypes.Markdown;
|
||||
case 'docx':
|
||||
return ContentTypes.Docx;
|
||||
default:
|
||||
return ContentTypes.OctetStream;
|
||||
}
|
||||
};
|
||||
|
||||
export const importDoc = async (file: File): Promise<Doc> => {
|
||||
const form = new FormData();
|
||||
|
||||
form.append(
|
||||
'file',
|
||||
new File([file], file.name, {
|
||||
type: getMimeType(file),
|
||||
lastModified: file.lastModified,
|
||||
}),
|
||||
);
|
||||
|
||||
const response = await fetchAPI(`documents/`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
withoutContentType: true,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError('Failed to import the doc', await errorCauses(response));
|
||||
}
|
||||
|
||||
return response.json() as Promise<Doc>;
|
||||
};
|
||||
|
||||
type UseImportDocOptions = UseMutationOptions<Doc, APIError, File>;
|
||||
|
||||
export function useImportDoc(props?: UseImportDocOptions) {
|
||||
const { toast } = useToastProvider();
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation<Doc, APIError, File>({
|
||||
mutationFn: importDoc,
|
||||
...props,
|
||||
onSuccess: (...successProps) => {
|
||||
queryClient.setQueriesData<UseInfiniteQueryResultAPI<DocsResponse>>(
|
||||
{ queryKey: [KEY_LIST_DOC] },
|
||||
(oldData) => {
|
||||
if (!oldData || oldData?.pages.length === 0) {
|
||||
return oldData;
|
||||
}
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
pages: oldData.pages.map((page, index) => {
|
||||
// Add the new doc to the first page only
|
||||
if (index === 0) {
|
||||
return {
|
||||
...page,
|
||||
results: [successProps[0], ...page.results],
|
||||
};
|
||||
}
|
||||
return page;
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
toast(
|
||||
t('The document "{{documentName}}" has been successfully imported', {
|
||||
documentName: successProps?.[0].title || '',
|
||||
}),
|
||||
VariantType.SUCCESS,
|
||||
);
|
||||
|
||||
props?.onSuccess?.(...successProps);
|
||||
},
|
||||
onError: (...errorProps) => {
|
||||
toast(
|
||||
t(`The document "{{documentName}}" import has failed`, {
|
||||
documentName: errorProps?.[1].name || '',
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
);
|
||||
|
||||
props?.onError?.(...errorProps);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
Button,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { InView } from 'react-intersection-observer';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Card, Text } from '@/components';
|
||||
import AllDocs from '@/assets/icons/doc-all.svg';
|
||||
import { Box, Card, Icon, Text } from '@/components';
|
||||
import { DocDefaultFilter, useInfiniteDocs } from '@/docs/doc-management';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useInfiniteDocsTrashbin } from '../api';
|
||||
import { ContentTypesAllowed, useImportDoc } from '../api/useImportDoc';
|
||||
import { useResponsiveDocGrid } from '../hooks/useResponsiveDocGrid';
|
||||
|
||||
import {
|
||||
@@ -24,6 +31,44 @@ export const DocsGrid = ({
|
||||
target = DocDefaultFilter.ALL_DOCS,
|
||||
}: DocsGridProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const { toast } = useToastProvider();
|
||||
const { getRootProps, getInputProps, open } = useDropzone({
|
||||
accept: {
|
||||
[ContentTypesAllowed.Docx]: ['.docx'],
|
||||
[ContentTypesAllowed.Markdown]: ['.md'],
|
||||
},
|
||||
onDrop(acceptedFiles) {
|
||||
setIsDragOver(false);
|
||||
for (const file of acceptedFiles) {
|
||||
importDoc(file);
|
||||
}
|
||||
},
|
||||
onDragEnter: () => {
|
||||
setIsDragOver(true);
|
||||
},
|
||||
onDragLeave: () => {
|
||||
setIsDragOver(false);
|
||||
},
|
||||
onDropRejected(fileRejections) {
|
||||
toast(
|
||||
t(
|
||||
`The document "{{documentName}}" import has failed (only .docx and .md files are allowed)`,
|
||||
{
|
||||
documentName: fileRejections?.[0].file.name || '',
|
||||
},
|
||||
),
|
||||
VariantType.ERROR,
|
||||
);
|
||||
},
|
||||
noClick: true,
|
||||
});
|
||||
const { mutate: importDoc } = useImportDoc();
|
||||
|
||||
const withUpload =
|
||||
!target ||
|
||||
target === DocDefaultFilter.ALL_DOCS ||
|
||||
target === DocDefaultFilter.MY_DOCS;
|
||||
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { flexLeft, flexRight } = useResponsiveDocGrid();
|
||||
@@ -60,21 +105,6 @@ export const DocsGrid = ({
|
||||
void fetchNextPage();
|
||||
};
|
||||
|
||||
let title = t('All docs');
|
||||
switch (target) {
|
||||
case DocDefaultFilter.MY_DOCS:
|
||||
title = t('My docs');
|
||||
break;
|
||||
case DocDefaultFilter.SHARED_WITH_ME:
|
||||
title = t('Shared with me');
|
||||
break;
|
||||
case DocDefaultFilter.TRASHBIN:
|
||||
title = t('Trashbin');
|
||||
break;
|
||||
default:
|
||||
title = t('All docs');
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
$position="relative"
|
||||
@@ -91,16 +121,24 @@ export const DocsGrid = ({
|
||||
$width="100%"
|
||||
$css={css`
|
||||
${!isDesktop ? 'border: none;' : ''}
|
||||
${isDragOver
|
||||
? `
|
||||
border: 2px dashed var(--c--contextuals--border--semantic--brand--primary);
|
||||
background-color: var(--c--contextuals--background--semantic--brand--tertiary);
|
||||
`
|
||||
: ''}
|
||||
`}
|
||||
$padding={{
|
||||
top: 'base',
|
||||
horizontal: isDesktop ? 'md' : 'xs',
|
||||
bottom: 'md',
|
||||
}}
|
||||
{...(withUpload ? getRootProps({ className: 'dropzone' }) : {})}
|
||||
>
|
||||
<Text as="h2" $size="h4" $margin={{ top: '0px', bottom: '10px' }}>
|
||||
{title}
|
||||
</Text>
|
||||
{withUpload && <input {...getInputProps()} />}
|
||||
<DocGridTitleBar
|
||||
target={target}
|
||||
onUploadClick={open}
|
||||
withUpload={withUpload}
|
||||
/>
|
||||
|
||||
{!hasDocs && !loading && (
|
||||
<Box $padding={{ vertical: 'sm' }} $align="center" $justify="center">
|
||||
@@ -110,7 +148,11 @@ export const DocsGrid = ({
|
||||
</Box>
|
||||
)}
|
||||
{hasDocs && (
|
||||
<Box $gap="6px" $overflow="auto">
|
||||
<Box
|
||||
$gap="6px"
|
||||
$overflow="auto"
|
||||
$padding={{ vertical: 'sm', horizontal: isDesktop ? 'md' : 'xs' }}
|
||||
>
|
||||
<Box role="grid" aria-label={t('Documents grid')}>
|
||||
<Box role="rowgroup">
|
||||
<Box
|
||||
@@ -172,6 +214,73 @@ export const DocsGrid = ({
|
||||
);
|
||||
};
|
||||
|
||||
const DocGridTitleBar = ({
|
||||
target,
|
||||
onUploadClick,
|
||||
withUpload,
|
||||
}: {
|
||||
target: DocDefaultFilter;
|
||||
onUploadClick: () => void;
|
||||
withUpload: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
|
||||
let title = t('All docs');
|
||||
let icon = <Icon icon={<AllDocs width={24} height={24} />} />;
|
||||
switch (target) {
|
||||
case DocDefaultFilter.MY_DOCS:
|
||||
icon = <Icon iconName="lock" />;
|
||||
title = t('My docs');
|
||||
break;
|
||||
case DocDefaultFilter.SHARED_WITH_ME:
|
||||
icon = <Icon iconName="group" />;
|
||||
title = t('Shared with me');
|
||||
break;
|
||||
case DocDefaultFilter.TRASHBIN:
|
||||
icon = <Icon iconName="delete" />;
|
||||
title = t('Trashbin');
|
||||
break;
|
||||
default:
|
||||
title = t('All docs');
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
$direction="row"
|
||||
$padding={{
|
||||
vertical: 'md',
|
||||
horizontal: isDesktop ? 'md' : 'xs',
|
||||
}}
|
||||
$css={css`
|
||||
border-bottom: 1px solid var(--c--contextuals--border--surface--primary);
|
||||
`}
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
>
|
||||
<Box $direction="row" $gap="xs" $align="center">
|
||||
{icon}
|
||||
<Text as="h2" $size="h4" $margin="none">
|
||||
{title}
|
||||
</Text>
|
||||
</Box>
|
||||
{withUpload && (
|
||||
<Button
|
||||
color="brand"
|
||||
variant="tertiary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUploadClick();
|
||||
}}
|
||||
aria-label={t('Open the upload dialog')}
|
||||
>
|
||||
<Icon iconName="upload_file" $withThemeInherited />
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const useDocsQuery = (target: DocDefaultFilter) => {
|
||||
const trashbinQuery = useInfiniteDocsTrashbin(
|
||||
{
|
||||
|
||||
@@ -29,6 +29,7 @@ export const Header = () => {
|
||||
<SkipToContent />
|
||||
<Box
|
||||
as="header"
|
||||
className="--docs--header"
|
||||
role="banner"
|
||||
$css={css`
|
||||
position: fixed;
|
||||
@@ -45,7 +46,6 @@ export const Header = () => {
|
||||
border-bottom: 1px solid
|
||||
var(--c--contextuals--border--surface--primary);
|
||||
`}
|
||||
className="--docs--header"
|
||||
>
|
||||
{!isDesktop && <ButtonTogglePanel />}
|
||||
<StyledLink
|
||||
@@ -88,7 +88,12 @@ export const Header = () => {
|
||||
<LaGaufre />
|
||||
</Box>
|
||||
) : (
|
||||
<Box $align="center" $gap={spacingsTokens['sm']} $direction="row">
|
||||
<Box
|
||||
className="--docs--header-block-right"
|
||||
$align="center"
|
||||
$gap={spacingsTokens['sm']}
|
||||
$direction="row"
|
||||
>
|
||||
<ButtonLogin />
|
||||
<LanguagePicker />
|
||||
<LaGaufre />
|
||||
|
||||
+6
-5
@@ -2,6 +2,7 @@ import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import AllDocs from '@/assets/icons/doc-all.svg';
|
||||
import { Box, Icon, StyledLink, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { DocDefaultFilter } from '@/docs/doc-management';
|
||||
@@ -21,22 +22,22 @@ export const LeftPanelTargetFilters = () => {
|
||||
|
||||
const defaultQueries = [
|
||||
{
|
||||
icon: 'apps',
|
||||
icon: <Icon icon={<AllDocs width={24} height={24} />} />,
|
||||
label: t('All docs'),
|
||||
targetQuery: DocDefaultFilter.ALL_DOCS,
|
||||
},
|
||||
{
|
||||
icon: 'lock',
|
||||
icon: <Icon iconName="lock" />,
|
||||
label: t('My docs'),
|
||||
targetQuery: DocDefaultFilter.MY_DOCS,
|
||||
},
|
||||
{
|
||||
icon: 'group',
|
||||
icon: <Icon iconName="group" />,
|
||||
label: t('Shared with me'),
|
||||
targetQuery: DocDefaultFilter.SHARED_WITH_ME,
|
||||
},
|
||||
{
|
||||
icon: 'delete',
|
||||
icon: <Icon iconName="delete" />,
|
||||
label: t('Trashbin'),
|
||||
targetQuery: DocDefaultFilter.TRASHBIN,
|
||||
},
|
||||
@@ -96,7 +97,7 @@ export const LeftPanelTargetFilters = () => {
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Icon iconName={query.icon} />
|
||||
{query.icon}
|
||||
<Text $size="sm">{query.label}</Text>
|
||||
</StyledLink>
|
||||
);
|
||||
|
||||
@@ -715,6 +715,7 @@
|
||||
"All docs": "Tous les documents",
|
||||
"An error occurred while restoring the document: {{error}}": "Une erreur est survenue lors de la restauration du document : {{error}}",
|
||||
"An uncompromising writing experience.": "Une expérience d'écriture sans compromis.",
|
||||
"An unexpected error occurred.": "Une erreur inattendue s’est produite.",
|
||||
"Analyzing file...": "Analyse du fichier...",
|
||||
"Anonymous": "Anonyme",
|
||||
"Anyone with the link can edit the document": "N'importe qui avec le lien peut éditer le document",
|
||||
@@ -896,6 +897,7 @@
|
||||
"Quick search input": "Saisie de recherche rapide",
|
||||
"Reader": "Lecteur",
|
||||
"Reading": "Lecture seule",
|
||||
"Refresh page": "Actualiser la page",
|
||||
"Remove access": "Supprimer l'accès",
|
||||
"Remove emoji": "Supprimer les emojis",
|
||||
"Remove {{name}} from the invite list": "Retirer {{name}} de la liste d'invitation",
|
||||
@@ -956,6 +958,7 @@
|
||||
"Updated": "Mise à jour",
|
||||
"Updated at": "Mise à jour le",
|
||||
"Upload PDF": "Téléverser un PDF",
|
||||
"Use arrow keys to navigate between documents. Press Enter to open a document. Press F2 to focus the emoji button when available, then press F2 again to access document actions.": "Utilisez les touches fléchées pour vous déplacer entre les documents. Appuyez sur Entrée pour ouvrir un document. Appuyez sur F2 pour accéder au bouton Emoji, le cas échéant, puis appuyez à nouveau sur F2 pour accéder aux actions disponibles pour le document.",
|
||||
"Use as prompt": "Utiliser comme un prompt",
|
||||
"Version history": "Historique des versions",
|
||||
"Version restored successfully": "Version restaurée avec succès",
|
||||
@@ -1150,6 +1153,7 @@
|
||||
"All docs": "Alle documenten",
|
||||
"An error occurred while restoring the document: {{error}}": "Er is een fout opgetreden tijdens het herstellen van het document: {{error}}",
|
||||
"An uncompromising writing experience.": "Een compromisloze schrijfervaring.",
|
||||
"An unexpected error occurred.": "Er deed zich een onverwachte fout voor.",
|
||||
"Analyzing file...": "Bestand analyseren...",
|
||||
"Anonymous": "Anoniem",
|
||||
"Anyone with the link can edit the document": "Iedereen met de link kan het document bewerken",
|
||||
@@ -1331,6 +1335,7 @@
|
||||
"Quick search input": "Snel zoeken",
|
||||
"Reader": "Lezer",
|
||||
"Reading": "Lezen",
|
||||
"Refresh page": "Pagina vernieuwen",
|
||||
"Remove access": "Toegang verwijderen",
|
||||
"Remove emoji": "Emoji verwijderen",
|
||||
"Remove {{name}} from the invite list": "Verwijder {{name}} uit de uitnodigingslijst",
|
||||
@@ -1391,6 +1396,7 @@
|
||||
"Updated": "Bijgewerkt",
|
||||
"Updated at": "Bijgewerkt op",
|
||||
"Upload PDF": "PDF uploaden",
|
||||
"Use arrow keys to navigate between documents. Press Enter to open a document. Press F2 to focus the emoji button when available, then press F2 again to access document actions.": "Gebruik de pijltjestoetsen om tussen documenten te navigeren. Druk op Enter om een document te openen. Druk op F2 om de emoji knop te focussen wanneer beschikbaar en druk daarna opnieuw op F2 om documentacties te openen.",
|
||||
"Use as prompt": "Gebruik als prompt",
|
||||
"Version history": "Versiegeschiedenis",
|
||||
"Version restored successfully": "Versie succesvol hersteld",
|
||||
@@ -1446,6 +1452,7 @@
|
||||
"All docs": "Все документы",
|
||||
"An error occurred while restoring the document: {{error}}": "При восстановлении документа произошла ошибка: {{error}}",
|
||||
"An uncompromising writing experience.": "Бескомпромиссный опыт написания.",
|
||||
"An unexpected error occurred.": "Произошла непредвиденная ошибка.",
|
||||
"Analyzing file...": "Анализ файла...",
|
||||
"Anonymous": "Аноним",
|
||||
"Anyone with the link can edit the document": "Любой, у кого есть ссылка, может редактировать документ",
|
||||
@@ -1627,6 +1634,7 @@
|
||||
"Quick search input": "Быстрый поиск",
|
||||
"Reader": "Читатель",
|
||||
"Reading": "Чтение",
|
||||
"Refresh page": "Обновить страницу",
|
||||
"Remove access": "Отменить доступ",
|
||||
"Remove emoji": "Убрать эмодзи",
|
||||
"Remove {{name}} from the invite list": "Удалить {{name}} из списка приглашений",
|
||||
@@ -1687,6 +1695,7 @@
|
||||
"Updated": "Обновлено",
|
||||
"Updated at": "Обновлено",
|
||||
"Upload PDF": "Выгрузить PDF",
|
||||
"Use arrow keys to navigate between documents. Press Enter to open a document. Press F2 to focus the emoji button when available, then press F2 again to access document actions.": "Используйте клавиши со стрелками для перемещения между документами. Нажмите Enter для открытия документа. Нажмите F2, чтобы перейти к кнопке эмодзи, при их наличии, затем снова нажмите F2 для доступа к действиям с документом.",
|
||||
"Use as prompt": "Использовать как запрос",
|
||||
"Version history": "История версий",
|
||||
"Version restored successfully": "Версия успешно восстановлена",
|
||||
@@ -1845,6 +1854,7 @@
|
||||
"All docs": "Всі документи",
|
||||
"An error occurred while restoring the document: {{error}}": "При відновленні документа виникла помилка: {{error}}",
|
||||
"An uncompromising writing experience.": "Безкомпромісне задоволення від процесу письма.",
|
||||
"An unexpected error occurred.": "Сталася неочікувана помилка.",
|
||||
"Analyzing file...": "Аналіз файлу...",
|
||||
"Anonymous": "Анонім",
|
||||
"Anyone with the link can edit the document": "Будь-хто з посиланням може редагувати документ",
|
||||
@@ -2026,6 +2036,7 @@
|
||||
"Quick search input": "Швидкий пошук",
|
||||
"Reader": "Читач",
|
||||
"Reading": "Читання",
|
||||
"Refresh page": "Оновити сторінку",
|
||||
"Remove access": "Вилучити доступ",
|
||||
"Remove emoji": "Прибрати емодзі",
|
||||
"Remove {{name}} from the invite list": "Видалити {{name}} зі списку запрошень",
|
||||
@@ -2086,6 +2097,7 @@
|
||||
"Updated": "Оновлено",
|
||||
"Updated at": "Оновлено",
|
||||
"Upload PDF": "Вивантажити PDF",
|
||||
"Use arrow keys to navigate between documents. Press Enter to open a document. Press F2 to focus the emoji button when available, then press F2 again to access document actions.": "Використовуйте клавіші зі стрілками для переміщення між документами. Натисніть Enter, щоб відкрити документ. Натисніть F2, щоб перейти до кнопки емодзі, якщо вони є, потім знову натисніть F2, щоб отримати доступ до дій з документом.",
|
||||
"Use as prompt": "Використати як запит",
|
||||
"Version history": "Історія версій",
|
||||
"Version restored successfully": "Версія успішно відновлена",
|
||||
|
||||
@@ -120,7 +120,7 @@ const MainContent = ({
|
||||
$css={css`
|
||||
overflow-y: auto;
|
||||
overflow-x: clip;
|
||||
&:focus {
|
||||
&:focus-visible {
|
||||
outline: 3px solid ${colorsTokens['brand-400']};
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@ body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line selector-id-pattern */
|
||||
body > #__next > .c__app > div:has(> .c__loader) {
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { HOME_URL } from '@/features/auth';
|
||||
|
||||
const Page = () => {
|
||||
const { replace } = useRouter();
|
||||
void replace(HOME_URL);
|
||||
window.location.replace(HOME_URL);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -3,6 +3,7 @@ import posthog from 'posthog-js';
|
||||
import { PostHogProvider as PHProvider } from 'posthog-js/react';
|
||||
import { JSX, PropsWithChildren, ReactNode, useEffect } from 'react';
|
||||
|
||||
import { useIsOffline } from '@/features/service-worker/hooks/useOffline';
|
||||
import { AbstractAnalytic, AnalyticEvent } from '@/libs/';
|
||||
|
||||
export class PostHogAnalytic extends AbstractAnalytic {
|
||||
@@ -45,6 +46,8 @@ export function PostHogProvider({
|
||||
children,
|
||||
conf,
|
||||
}: PropsWithChildren<PostHogProviderProps>) {
|
||||
const isOffline = useIsOffline((state) => state.isOffline);
|
||||
|
||||
useEffect(() => {
|
||||
if (!conf?.id || !conf?.host || posthog.__loaded) {
|
||||
return;
|
||||
@@ -53,9 +56,9 @@ export function PostHogProvider({
|
||||
posthog.init(conf.id, {
|
||||
api_host: conf.host,
|
||||
person_profiles: 'always',
|
||||
loaded: (posthog) => {
|
||||
loaded: (posthogInstance) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
posthog.debug();
|
||||
posthogInstance.debug();
|
||||
}
|
||||
},
|
||||
capture_pageview: false,
|
||||
@@ -71,5 +74,14 @@ export function PostHogProvider({
|
||||
};
|
||||
}, [conf?.host, conf?.id]);
|
||||
|
||||
// Disable PostHog when offline to prevent retry requests
|
||||
useEffect(() => {
|
||||
if (isOffline) {
|
||||
posthog.opt_out_capturing();
|
||||
} else {
|
||||
posthog.opt_in_capturing();
|
||||
}
|
||||
}, [isOffline]);
|
||||
|
||||
return <PHProvider client={posthog}>{children}</PHProvider>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impress",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"private": true,
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"author": "DINUM",
|
||||
@@ -31,14 +31,15 @@
|
||||
"server:test": "yarn COLLABORATION_SERVER run test"
|
||||
},
|
||||
"resolutions": {
|
||||
"@tiptap/extensions": "3.11.0",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/react": "19.2.6",
|
||||
"@tiptap/extensions": "3.13.0",
|
||||
"@types/node": "24.10.4",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"docx": "9.5.0",
|
||||
"eslint": "9.39.1",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"eslint": "9.39.2",
|
||||
"prosemirror-view": "1.41.3",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"typescript": "5.9.3",
|
||||
"wrap-ansi": "9.0.2",
|
||||
"yjs": "13.6.27"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eslint-plugin-docs",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"author": "DINUM",
|
||||
"license": "MIT",
|
||||
@@ -17,21 +17,21 @@
|
||||
"eslint": ">=9.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "16.0.3",
|
||||
"@next/eslint-plugin-next": "15.5.9",
|
||||
"@tanstack/eslint-plugin-query": "5.91.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.47.0",
|
||||
"@typescript-eslint/parser": "8.47.0",
|
||||
"@vitest/eslint-plugin": "1.4.3",
|
||||
"eslint-config-next": "16.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "8.49.0",
|
||||
"@typescript-eslint/parser": "8.49.0",
|
||||
"@vitest/eslint-plugin": "1.5.2",
|
||||
"eslint-config-next": "15.5.9",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-jest": "29.2.1",
|
||||
"eslint-plugin-jest": "29.5.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-playwright": "2.3.0",
|
||||
"eslint-plugin-playwright": "2.4.0",
|
||||
"eslint-plugin-prettier": "5.5.4",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-testing-library": "7.13.5",
|
||||
"prettier": "3.6.2"
|
||||
"eslint-plugin-testing-library": "7.13.6",
|
||||
"prettier": "3.7.4"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "packages-i18n",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"author": "DINUM",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"i18next-parser": "9.3.0",
|
||||
"jest": "30.2.0",
|
||||
"ts-jest": "29.4.5",
|
||||
"ts-jest": "29.4.6",
|
||||
"typescript": "*",
|
||||
"yargs": "18.0.0"
|
||||
},
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('Server Tests', () => {
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('origin', origin)
|
||||
.set('authorization', 'wrong-api-key')
|
||||
.set('authorization', `Bearer wrong-api-key`)
|
||||
.set('content-type', 'application/json');
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
@@ -99,7 +99,7 @@ describe('Server Tests', () => {
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('origin', origin)
|
||||
.set('authorization', apiKey)
|
||||
.set('authorization', `Bearer ${apiKey}`)
|
||||
.set('content-type', 'application/json');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
@@ -114,7 +114,7 @@ describe('Server Tests', () => {
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('origin', origin)
|
||||
.set('authorization', apiKey)
|
||||
.set('authorization', `Bearer ${apiKey}`)
|
||||
.set('content-type', 'application/json')
|
||||
.send('');
|
||||
|
||||
@@ -129,9 +129,10 @@ describe('Server Tests', () => {
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('origin', origin)
|
||||
.set('authorization', apiKey)
|
||||
.set('authorization', `Bearer ${apiKey}`)
|
||||
.set('content-type', 'image/png')
|
||||
.send('randomdata');
|
||||
|
||||
expect(response.status).toBe(415);
|
||||
expect(response.body).toStrictEqual({ error: 'Unsupported Content-Type' });
|
||||
});
|
||||
@@ -141,38 +142,73 @@ describe('Server Tests', () => {
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('origin', origin)
|
||||
.set('authorization', apiKey)
|
||||
.set('authorization', `Bearer ${apiKey}`)
|
||||
.set('content-type', 'text/markdown')
|
||||
.set('accept', 'image/png')
|
||||
.send('# Header');
|
||||
|
||||
expect(response.status).toBe(406);
|
||||
expect(response.body).toStrictEqual({ error: 'Unsupported format' });
|
||||
});
|
||||
|
||||
test.each([[apiKey], [`Bearer ${apiKey}`]])(
|
||||
'POST /api/convert with correct content with Authorization: %s',
|
||||
async (authHeader) => {
|
||||
const app = initApp();
|
||||
test('POST /api/convert BlockNote to Markdown', async () => {
|
||||
const app = initApp();
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('origin', origin)
|
||||
.set('authorization', `Bearer ${apiKey}`)
|
||||
.set('content-type', 'application/vnd.blocknote+json')
|
||||
.set('accept', 'text/markdown')
|
||||
.send(expectedBlocks);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('Origin', origin)
|
||||
.set('Authorization', authHeader)
|
||||
.set('content-type', 'text/markdown')
|
||||
.set('accept', 'application/vnd.yjs.doc')
|
||||
.send(expectedMarkdown);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.header['content-type']).toBe(
|
||||
'text/markdown; charset=utf-8',
|
||||
);
|
||||
expect(typeof response.text).toBe('string');
|
||||
expect(response.text.trim()).toBe(expectedMarkdown);
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toBeInstanceOf(Buffer);
|
||||
test('POST /api/convert BlockNote to Yjs', async () => {
|
||||
const app = initApp();
|
||||
const editor = ServerBlockNoteEditor.create();
|
||||
const blocks = await editor.tryParseMarkdownToBlocks(expectedMarkdown);
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('origin', origin)
|
||||
.set('authorization', `Bearer ${apiKey}`)
|
||||
.set('content-type', 'application/vnd.blocknote+json')
|
||||
.set('accept', 'application/vnd.yjs.doc')
|
||||
.send(blocks)
|
||||
.responseType('blob');
|
||||
|
||||
const editor = ServerBlockNoteEditor.create();
|
||||
const doc = new Y.Doc();
|
||||
Y.applyUpdate(doc, response.body);
|
||||
const blocks = editor.yDocToBlocks(doc, 'document-store');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.header['content-type']).toBe('application/vnd.yjs.doc');
|
||||
|
||||
expect(blocks).toStrictEqual(expectedBlocks);
|
||||
},
|
||||
);
|
||||
// Decode the Yjs response and verify it contains the correct blocks
|
||||
const responseBuffer = Buffer.from(response.body as Buffer);
|
||||
const ydoc = new Y.Doc();
|
||||
Y.applyUpdate(ydoc, responseBuffer);
|
||||
const decodedBlocks = editor.yDocToBlocks(ydoc, 'document-store');
|
||||
|
||||
expect(decodedBlocks).toStrictEqual(expectedBlocks);
|
||||
});
|
||||
|
||||
test('POST /api/convert BlockNote to HTML', async () => {
|
||||
const app = initApp();
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('origin', origin)
|
||||
.set('authorization', `Bearer ${apiKey}`)
|
||||
.set('content-type', 'application/vnd.blocknote+json')
|
||||
.set('accept', 'text/html')
|
||||
.send(expectedBlocks);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.header['content-type']).toBe('text/html; charset=utf-8');
|
||||
expect(typeof response.text).toBe('string');
|
||||
expect(response.text).toBe(expectedHTML);
|
||||
});
|
||||
|
||||
test('POST /api/convert Yjs to HTML', async () => {
|
||||
const app = initApp();
|
||||
@@ -183,10 +219,11 @@ describe('Server Tests', () => {
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('origin', origin)
|
||||
.set('authorization', apiKey)
|
||||
.set('authorization', `Bearer ${apiKey}`)
|
||||
.set('content-type', 'application/vnd.yjs.doc')
|
||||
.set('accept', 'text/html')
|
||||
.send(Buffer.from(yjsUpdate));
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.header['content-type']).toBe('text/html; charset=utf-8');
|
||||
expect(typeof response.text).toBe('string');
|
||||
@@ -202,10 +239,11 @@ describe('Server Tests', () => {
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('origin', origin)
|
||||
.set('authorization', apiKey)
|
||||
.set('authorization', `Bearer ${apiKey}`)
|
||||
.set('content-type', 'application/vnd.yjs.doc')
|
||||
.set('accept', 'text/markdown')
|
||||
.send(Buffer.from(yjsUpdate));
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.header['content-type']).toBe(
|
||||
'text/markdown; charset=utf-8',
|
||||
@@ -223,15 +261,16 @@ describe('Server Tests', () => {
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('origin', origin)
|
||||
.set('authorization', apiKey)
|
||||
.set('authorization', `Bearer ${apiKey}`)
|
||||
.set('content-type', 'application/vnd.yjs.doc')
|
||||
.set('accept', 'application/json')
|
||||
.send(Buffer.from(yjsUpdate));
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.header['content-type']).toBe(
|
||||
'application/json; charset=utf-8',
|
||||
);
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body).toBeInstanceOf(Array);
|
||||
expect(response.body).toStrictEqual(expectedBlocks);
|
||||
});
|
||||
|
||||
@@ -240,15 +279,16 @@ describe('Server Tests', () => {
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('origin', origin)
|
||||
.set('authorization', apiKey)
|
||||
.set('authorization', `Bearer ${apiKey}`)
|
||||
.set('content-type', 'text/markdown')
|
||||
.set('accept', 'application/json')
|
||||
.send(expectedMarkdown);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.header['content-type']).toBe(
|
||||
'application/json; charset=utf-8',
|
||||
);
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body).toBeInstanceOf(Array);
|
||||
expect(response.body).toStrictEqual(expectedBlocks);
|
||||
});
|
||||
|
||||
@@ -257,11 +297,12 @@ describe('Server Tests', () => {
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('origin', origin)
|
||||
.set('authorization', apiKey)
|
||||
.set('authorization', `Bearer ${apiKey}`)
|
||||
.set('content-type', 'application/vnd.yjs.doc')
|
||||
.set('accept', 'application/json')
|
||||
.send(Buffer.from('notvalidyjs'));
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toStrictEqual({ error: 'Invalid Yjs content' });
|
||||
expect(response.body).toStrictEqual({ error: 'Invalid content' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server-y-provider",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"description": "Y.js provider for docs",
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"license": "MIT",
|
||||
@@ -16,24 +16,24 @@
|
||||
"node": ">=22"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocknote/server-util": "0.44.2",
|
||||
"@hocuspocus/server": "3.4.0",
|
||||
"@sentry/node": "10.26.0",
|
||||
"@sentry/profiling-node": "10.26.0",
|
||||
"@blocknote/server-util": "0.45.0",
|
||||
"@hocuspocus/server": "3.4.3",
|
||||
"@sentry/node": "10.30.0",
|
||||
"@sentry/profiling-node": "10.30.0",
|
||||
"@tiptap/extensions": "*",
|
||||
"axios": "1.13.2",
|
||||
"cors": "2.8.5",
|
||||
"express": "5.1.0",
|
||||
"express": "5.2.1",
|
||||
"express-ws": "5.0.2",
|
||||
"uuid": "13.0.0",
|
||||
"y-protocols": "1.0.6",
|
||||
"yjs": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocknote/core": "0.44.2",
|
||||
"@hocuspocus/provider": "3.4.0",
|
||||
"@blocknote/core": "0.45.0",
|
||||
"@hocuspocus/provider": "3.4.3",
|
||||
"@types/cors": "2.8.19",
|
||||
"@types/express": "5.0.5",
|
||||
"@types/express": "5.0.6",
|
||||
"@types/express-ws": "3.0.6",
|
||||
"@types/node": "*",
|
||||
"@types/supertest": "6.0.3",
|
||||
@@ -45,7 +45,7 @@
|
||||
"ts-node": "10.9.2",
|
||||
"tsc-alias": "1.8.16",
|
||||
"typescript": "*",
|
||||
"vitest": "4.0.13",
|
||||
"vitest": "4.0.15",
|
||||
"vitest-mock-extended": "3.1.0",
|
||||
"ws": "8.18.3"
|
||||
},
|
||||
|
||||
@@ -14,27 +14,115 @@ interface ErrorResponse {
|
||||
error: string;
|
||||
}
|
||||
|
||||
type ConversionResponseBody = Uint8Array | string | object | ErrorResponse;
|
||||
|
||||
interface InputReader {
|
||||
supportedContentTypes: string[];
|
||||
read(data: Buffer): Promise<PartialBlock[]>;
|
||||
}
|
||||
|
||||
interface OutputWriter {
|
||||
supportedContentTypes: string[];
|
||||
write(blocks: PartialBlock[]): Promise<ConversionResponseBody>;
|
||||
}
|
||||
|
||||
const editor = ServerBlockNoteEditor.create<
|
||||
DefaultBlockSchema,
|
||||
DefaultInlineContentSchema,
|
||||
DefaultStyleSchema
|
||||
>();
|
||||
|
||||
const ContentTypes = {
|
||||
XMarkdown: 'text/x-markdown',
|
||||
Markdown: 'text/markdown',
|
||||
YJS: 'application/vnd.yjs.doc',
|
||||
FormUrlEncoded: 'application/x-www-form-urlencoded',
|
||||
OctetStream: 'application/octet-stream',
|
||||
HTML: 'text/html',
|
||||
BlockNote: 'application/vnd.blocknote+json',
|
||||
JSON: 'application/json',
|
||||
} as const;
|
||||
|
||||
const createYDocument = (blocks: PartialBlock[]) =>
|
||||
editor.blocksToYDoc(blocks, 'document-store');
|
||||
|
||||
const readers: InputReader[] = [
|
||||
{
|
||||
// application/x-www-form-urlencoded is interpreted as Markdown for backward compatibility
|
||||
supportedContentTypes: [
|
||||
ContentTypes.Markdown,
|
||||
ContentTypes.XMarkdown,
|
||||
ContentTypes.FormUrlEncoded,
|
||||
],
|
||||
read: (data) => editor.tryParseMarkdownToBlocks(data.toString()),
|
||||
},
|
||||
{
|
||||
supportedContentTypes: [ContentTypes.YJS, ContentTypes.OctetStream],
|
||||
read: async (data) => {
|
||||
const ydoc = new Y.Doc();
|
||||
Y.applyUpdate(ydoc, data);
|
||||
return editor.yDocToBlocks(ydoc, 'document-store') as PartialBlock[];
|
||||
},
|
||||
},
|
||||
{
|
||||
supportedContentTypes: [ContentTypes.BlockNote],
|
||||
read: async (data) => JSON.parse(data.toString()),
|
||||
},
|
||||
];
|
||||
|
||||
const writers: OutputWriter[] = [
|
||||
{
|
||||
supportedContentTypes: [ContentTypes.BlockNote, ContentTypes.JSON],
|
||||
write: async (blocks) => blocks,
|
||||
},
|
||||
{
|
||||
supportedContentTypes: [ContentTypes.YJS, ContentTypes.OctetStream],
|
||||
write: async (blocks) => Y.encodeStateAsUpdate(createYDocument(blocks)),
|
||||
},
|
||||
{
|
||||
supportedContentTypes: [ContentTypes.Markdown, ContentTypes.XMarkdown],
|
||||
write: (blocks) => editor.blocksToMarkdownLossy(blocks),
|
||||
},
|
||||
{
|
||||
supportedContentTypes: [ContentTypes.HTML],
|
||||
write: (blocks) => editor.blocksToHTMLLossy(blocks),
|
||||
},
|
||||
];
|
||||
|
||||
const normalizeContentType = (value: string) => value.split(';')[0];
|
||||
|
||||
export const convertHandler = async (
|
||||
req: Request<object, Uint8Array | ErrorResponse, Buffer, object>,
|
||||
res: Response<Uint8Array | string | object | ErrorResponse>,
|
||||
res: Response<ConversionResponseBody>,
|
||||
) => {
|
||||
if (!req.body || req.body.length === 0) {
|
||||
res.status(400).json({ error: 'Invalid request: missing content' });
|
||||
return;
|
||||
}
|
||||
|
||||
const contentType = (req.header('content-type') || 'text/markdown').split(
|
||||
';',
|
||||
)[0];
|
||||
const accept = (req.header('accept') || 'application/vnd.yjs.doc').split(
|
||||
';',
|
||||
)[0];
|
||||
const contentType = normalizeContentType(
|
||||
req.header('content-type') || ContentTypes.Markdown,
|
||||
);
|
||||
|
||||
const reader = readers.find((reader) =>
|
||||
reader.supportedContentTypes.includes(contentType),
|
||||
);
|
||||
|
||||
if (!reader) {
|
||||
res.status(415).json({ error: 'Unsupported Content-Type' });
|
||||
return;
|
||||
}
|
||||
|
||||
const accept = normalizeContentType(req.header('accept') || ContentTypes.YJS);
|
||||
|
||||
const writer = writers.find((writer) =>
|
||||
writer.supportedContentTypes.includes(accept),
|
||||
);
|
||||
|
||||
if (!writer) {
|
||||
res.status(406).json({ error: 'Unsupported format' });
|
||||
return;
|
||||
}
|
||||
|
||||
let blocks:
|
||||
| PartialBlock<
|
||||
@@ -44,63 +132,23 @@ export const convertHandler = async (
|
||||
>[]
|
||||
| null = null;
|
||||
try {
|
||||
// First, convert from the input format to blocks
|
||||
// application/x-www-form-urlencoded is interpreted as Markdown for backward compatibility
|
||||
if (
|
||||
contentType === 'text/markdown' ||
|
||||
contentType === 'application/x-www-form-urlencoded'
|
||||
) {
|
||||
blocks = await editor.tryParseMarkdownToBlocks(req.body.toString());
|
||||
} else if (
|
||||
contentType === 'application/vnd.yjs.doc' ||
|
||||
contentType === 'application/octet-stream'
|
||||
) {
|
||||
try {
|
||||
const ydoc = new Y.Doc();
|
||||
Y.applyUpdate(ydoc, req.body);
|
||||
blocks = editor.yDocToBlocks(ydoc, 'document-store') as PartialBlock[];
|
||||
} catch (e) {
|
||||
logger('Invalid Yjs content:', e);
|
||||
res.status(400).json({ error: 'Invalid Yjs content' });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(415).json({ error: 'Unsupported Content-Type' });
|
||||
try {
|
||||
blocks = await reader.read(req.body);
|
||||
} catch (e) {
|
||||
logger('Invalid content:', e);
|
||||
res.status(400).json({ error: 'Invalid content' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!blocks || blocks.length === 0) {
|
||||
res.status(500).json({ error: 'No valid blocks were generated' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Then, convert from blocks to the output format
|
||||
if (accept === 'application/json') {
|
||||
res.status(200).json(blocks);
|
||||
} else {
|
||||
const yDocument = editor.blocksToYDoc(blocks, 'document-store');
|
||||
|
||||
if (
|
||||
accept === 'application/vnd.yjs.doc' ||
|
||||
accept === 'application/octet-stream'
|
||||
) {
|
||||
res
|
||||
.status(200)
|
||||
.setHeader('content-type', 'application/octet-stream')
|
||||
.send(Y.encodeStateAsUpdate(yDocument));
|
||||
} else if (accept === 'text/markdown') {
|
||||
res
|
||||
.status(200)
|
||||
.setHeader('content-type', 'text/markdown')
|
||||
.send(await editor.blocksToMarkdownLossy(blocks));
|
||||
} else if (accept === 'text/html') {
|
||||
res
|
||||
.status(200)
|
||||
.setHeader('content-type', 'text/html')
|
||||
.send(await editor.blocksToHTMLLossy(blocks));
|
||||
} else {
|
||||
res.status(406).json({ error: 'Unsupported format' });
|
||||
}
|
||||
}
|
||||
res
|
||||
.status(200)
|
||||
.setHeader('content-type', accept)
|
||||
.send(await writer.write(blocks));
|
||||
} catch (e) {
|
||||
logger('conversion failed:', e);
|
||||
res.status(500).json({ error: 'An error occurred' });
|
||||
|
||||
+2432
-2790
File diff suppressed because it is too large
Load Diff
@@ -31,17 +31,18 @@ backend:
|
||||
LOGGING_LEVEL_HANDLERS_CONSOLE: ERROR
|
||||
LOGGING_LEVEL_LOGGERS_ROOT: INFO
|
||||
LOGGING_LEVEL_LOGGERS_APP: INFO
|
||||
OIDC_USERINFO_SHORTNAME_FIELD: "given_name"
|
||||
OIDC_USERINFO_FULLNAME_FIELDS: "given_name,usual_name"
|
||||
OIDC_USERINFO_SHORTNAME_FIELD: "first_name"
|
||||
OIDC_USERINFO_FULLNAME_FIELDS: "name"
|
||||
OIDC_OP_JWKS_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/certs
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/auth
|
||||
OIDC_OP_TOKEN_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/token
|
||||
OIDC_OP_USER_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/userinfo
|
||||
OIDC_OP_LOGOUT_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/logout
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS: "docs.127.0.0.1.nip.io"
|
||||
OIDC_RP_CLIENT_ID: docs
|
||||
OIDC_RP_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
|
||||
OIDC_RP_SIGN_ALGO: RS256
|
||||
OIDC_RP_SCOPES: "openid email given_name usual_name"
|
||||
OIDC_RP_SCOPES: "openid email profile"
|
||||
LOGIN_REDIRECT_URL: https://docs.127.0.0.1.nip.io
|
||||
LOGIN_REDIRECT_URL_FAILURE: https://docs.127.0.0.1.nip.io
|
||||
LOGOUT_REDIRECT_URL: https://docs.127.0.0.1.nip.io
|
||||
|
||||
@@ -32,17 +32,18 @@ backend:
|
||||
LOGGING_LEVEL_HANDLERS_CONSOLE: ERROR
|
||||
LOGGING_LEVEL_LOGGERS_ROOT: INFO
|
||||
LOGGING_LEVEL_LOGGERS_APP: INFO
|
||||
OIDC_USERINFO_SHORTNAME_FIELD: "given_name"
|
||||
OIDC_USERINFO_FULLNAME_FIELDS: "given_name,usual_name"
|
||||
OIDC_USERINFO_SHORTNAME_FIELD: "first_name"
|
||||
OIDC_USERINFO_FULLNAME_FIELDS: "name"
|
||||
OIDC_OP_JWKS_ENDPOINT: https://{{ .Values.feature }}-docs-keycloak.{{ .Values.domain }}/realms/docs/protocol/openid-connect/certs
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT: https://{{ .Values.feature }}-docs-keycloak.{{ .Values.domain }}/realms/docs/protocol/openid-connect/auth
|
||||
OIDC_OP_TOKEN_ENDPOINT: https://{{ .Values.feature }}-docs-keycloak.{{ .Values.domain }}/realms/docs/protocol/openid-connect/token
|
||||
OIDC_OP_USER_ENDPOINT: https://{{ .Values.feature }}-docs-keycloak.{{ .Values.domain }}/realms/docs/protocol/openid-connect/userinfo
|
||||
OIDC_OP_LOGOUT_ENDPOINT: https://{{ .Values.feature }}-docs-keycloak.{{ .Values.domain }}/realms/docs/protocol/openid-connect/logout
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS: "{{ .Values.feature }}-docs.{{ .Values.domain }}"
|
||||
OIDC_RP_CLIENT_ID: docs
|
||||
OIDC_RP_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
|
||||
OIDC_RP_SIGN_ALGO: RS256
|
||||
OIDC_RP_SCOPES: "openid email given_name usual_name"
|
||||
OIDC_RP_SCOPES: "openid email profile"
|
||||
LOGIN_REDIRECT_URL: https://{{ .Values.feature }}-docs.{{ .Values.domain }}
|
||||
LOGIN_REDIRECT_URL_FAILURE: https://{{ .Values.feature }}-docs.{{ .Values.domain }}
|
||||
LOGOUT_REDIRECT_URL: https://{{ .Values.feature }}-docs.{{ .Values.domain }}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
environments:
|
||||
dev:
|
||||
values:
|
||||
- version: 4.2.0
|
||||
- version: 4.3.0
|
||||
feature:
|
||||
values:
|
||||
- version: 4.2.0
|
||||
- version: 4.3.0
|
||||
feature: ci
|
||||
domain: example.com
|
||||
imageTag: demo
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
type: application
|
||||
name: docs
|
||||
version: 4.2.0
|
||||
version: 4.3.0
|
||||
appVersion: latest
|
||||
|
||||
@@ -39,6 +39,14 @@
|
||||
| `ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/proxy-read-timeout` | | `86400` |
|
||||
| `ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/proxy-send-timeout` | | `86400` |
|
||||
| `ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/upstream-hash-by` | | `$arg_room` |
|
||||
| `ingressRedirects.enabled` | whether to enable the Ingress Redirects or not | `false` |
|
||||
| `ingressRedirects.className` | IngressClass to use for the Ingress Redirects | `nil` |
|
||||
| `ingressRedirects.host` | Host for the Ingress Redirects | `impress.example.com` |
|
||||
| `ingressRedirects.tls.enabled` | Weather to enable TLS for the Ingress Redirects | `true` |
|
||||
| `ingressRedirects.tls.secretName` | Secret name for TLS config | `nil` |
|
||||
| `ingressRedirects.tls.additional[].secretName` | Secret name for additional TLS config | |
|
||||
| `ingressRedirects.tls.additional[].hosts[]` | Hosts for additional TLS config | |
|
||||
| `ingressRedirects.rules` | Rules for the Ingress Redirects | `[]` |
|
||||
| `ingressCollaborationApi.enabled` | whether to enable the Ingress or not | `false` |
|
||||
| `ingressCollaborationApi.className` | IngressClass to use for the Ingress | `nil` |
|
||||
| `ingressCollaborationApi.host` | Host for the Ingress | `impress.example.com` |
|
||||
@@ -113,15 +121,15 @@
|
||||
| `backend.job.annotations` | Annotations to add to the job [default: argocd.argoproj.io/hook: PostSync] | |
|
||||
| `backend.cronjobs` | Cronjob name, schedule, command | `[]` |
|
||||
| `backend.probes.liveness.path` | Configure path for backend HTTP liveness probe | `/__heartbeat__` |
|
||||
| `backend.probes.liveness.targetPort` | Configure port for backend HTTP liveness probe | `undefined` |
|
||||
| `backend.probes.liveness.targetPort` | Configure port for backend HTTP liveness probe | `nil` |
|
||||
| `backend.probes.liveness.initialDelaySeconds` | Configure initial delay for backend liveness probe | `10` |
|
||||
| `backend.probes.liveness.initialDelaySeconds` | Configure timeout for backend liveness probe | `10` |
|
||||
| `backend.probes.startup.path` | Configure path for backend HTTP startup probe | `undefined` |
|
||||
| `backend.probes.startup.targetPort` | Configure port for backend HTTP startup probe | `undefined` |
|
||||
| `backend.probes.startup.initialDelaySeconds` | Configure initial delay for backend startup probe | `undefined` |
|
||||
| `backend.probes.startup.initialDelaySeconds` | Configure timeout for backend startup probe | `undefined` |
|
||||
| `backend.probes.startup.path` | Configure path for backend HTTP startup probe | `nil` |
|
||||
| `backend.probes.startup.targetPort` | Configure port for backend HTTP startup probe | `nil` |
|
||||
| `backend.probes.startup.initialDelaySeconds` | Configure initial delay for backend startup probe | `nil` |
|
||||
| `backend.probes.startup.initialDelaySeconds` | Configure timeout for backend startup probe | `nil` |
|
||||
| `backend.probes.readiness.path` | Configure path for backend HTTP readiness probe | `/__lbheartbeat__` |
|
||||
| `backend.probes.readiness.targetPort` | Configure port for backend HTTP readiness probe | `undefined` |
|
||||
| `backend.probes.readiness.targetPort` | Configure port for backend HTTP readiness probe | `nil` |
|
||||
| `backend.probes.readiness.initialDelaySeconds` | Configure initial delay for backend readiness probe | `10` |
|
||||
| `backend.probes.readiness.initialDelaySeconds` | Configure timeout for backend readiness probe | `10` |
|
||||
| `backend.resources` | Resource requirements for the backend container | `{}` |
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
{{- if .Values.ingressRedirects.enabled }}
|
||||
{{- $fullName := include "impress.fullname" . -}}
|
||||
{{- $ns := .Release.Namespace -}}
|
||||
|
||||
{{- range $i, $r := .Values.ingressRedirects.rules }}
|
||||
{{- $host := $r.host | default $.Values.ingressRedirects.host -}}
|
||||
{{- $from := $r.from | default "/home" -}}
|
||||
{{- $to := required (printf "ingressRedirects.rules[%d].to is required" $i) $r.to -}}
|
||||
{{- $name := printf "%s-redirect-%s" $fullName (replace "/" "-" (trimAll "/" $from)) | trunc 63 | trimSuffix "-" -}}
|
||||
{{- if $i }}
|
||||
---
|
||||
{{- end }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $ns }}
|
||||
annotations:
|
||||
{{- if or (not $r.code) (eq (toString $r.code) "301") }}
|
||||
nginx.ingress.kubernetes.io/permanent-redirect: "{{ $to }}"
|
||||
{{- else }}
|
||||
nginx.ingress.kubernetes.io/temporal-redirect: "{{ $to }}"
|
||||
nginx.ingress.kubernetes.io/temporal-redirect-code: "{{ $r.code }}"
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if $.Values.ingressRedirects.className }}
|
||||
ingressClassName: {{ $.Values.ingressRedirects.className }}
|
||||
{{- end }}
|
||||
{{- if $.Values.ingressRedirects.tls.enabled }}
|
||||
tls:
|
||||
{{- if $host }}
|
||||
- secretName: {{ $.Values.ingressRedirects.tls.secretName | default (printf "%s-tls" $fullName) | quote }}
|
||||
hosts:
|
||||
- {{ $host | quote }}
|
||||
{{- end }}
|
||||
{{- range $.Values.ingressRedirects.tls.additional }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ $host }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ $from }}
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "impress.frontend.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.frontend.service.port }}
|
||||
- path: {{ printf "%s/" (trimSuffix "/" $from) }}
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "impress.frontend.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.frontend.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -85,6 +85,25 @@ ingressCollaborationWS:
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "86400"
|
||||
nginx.ingress.kubernetes.io/upstream-hash-by: $arg_room
|
||||
|
||||
## @param ingressRedirects.enabled whether to enable the Ingress Redirects or not
|
||||
## @param ingressRedirects.className IngressClass to use for the Ingress Redirects
|
||||
## @param ingressRedirects.host Host for the Ingress Redirects
|
||||
ingressRedirects:
|
||||
enabled: false
|
||||
className: null
|
||||
host: impress.example.com
|
||||
## @param ingressRedirects.tls.enabled Weather to enable TLS for the Ingress Redirects
|
||||
## @param ingressRedirects.tls.secretName Secret name for TLS config
|
||||
## @skip ingressRedirects.tls.additional
|
||||
## @extra ingressRedirects.tls.additional[].secretName Secret name for additional TLS config
|
||||
## @extra ingressRedirects.tls.additional[].hosts[] Hosts for additional TLS config
|
||||
tls:
|
||||
enabled: true
|
||||
secretName: null
|
||||
additional: []
|
||||
## @param ingressRedirects.rules Rules for the Ingress Redirects
|
||||
rules: []
|
||||
|
||||
## @param ingressCollaborationApi.enabled whether to enable the Ingress or not
|
||||
## @param ingressCollaborationApi.className IngressClass to use for the Ingress
|
||||
## @param ingressCollaborationApi.host Host for the Ingress
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.5.4",
|
||||
"mjml": "4.17.1"
|
||||
"mjml": "4.18.0"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
+193
-193
@@ -581,46 +581,46 @@ minimatch@^9.0.3, minimatch@^9.0.4:
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
|
||||
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
||||
|
||||
mjml-accordion@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-accordion/-/mjml-accordion-4.17.1.tgz#6ee3c016ea78a5a0ed29f3ec28c17d181028994f"
|
||||
integrity sha512-xl9oUbMp8aju6b1OZYqv3orE287ofGNEv09h2mFmzRTJxug7nJBFXs2I9v7dUVuWIBRk940PjnIVSuW+9bPvCA==
|
||||
mjml-accordion@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-accordion/-/mjml-accordion-4.18.0.tgz#4695289161fc631bd3f4c9546fedb84b42712ef8"
|
||||
integrity sha512-9PUmy2JxIOGgAaVHvgVYX21nVAo3o/+wJckTTF/YTLGAqB+nm+44buxRzaXxVk7qXRwbCNfE8c8mlGVNh7vB1g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-body@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-body/-/mjml-body-4.17.1.tgz#a4e4d2ee34abfbb45b74999ee49356b35830a0dc"
|
||||
integrity sha512-EfvVVfutARRjJ1jsOxxf2DY/ufqWswv9JKjbwu/Fu8h4havAcdmw2BDmX3vwXEzatqpL1l//YWOKlqUe9ZEs+A==
|
||||
mjml-body@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-body/-/mjml-body-4.18.0.tgz#20731dbbc4a92813b4c4f6ce417fc79e7a0b307f"
|
||||
integrity sha512-34AwX70/7NkRIajPsa5j6NySRiNrlLatTKhiLwTVFiVtrEFlfCcbeMNmdVixI3Ldvs8209ZC6euaAnXDRyR1zw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-button@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-button/-/mjml-button-4.17.1.tgz#1cbdf444802690329ea59ea524d088607666fa5f"
|
||||
integrity sha512-A9xQwgccPzrwr11izorBsA92THpkrviWkCwlYMxL9V3wgt5YJYDrt4r023HCveqN7b6iTkvqkXeDoIPX/kEDDQ==
|
||||
mjml-button@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-button/-/mjml-button-4.18.0.tgz#654a1fb6ade98c32f8c7c8e90c729edde81eeefb"
|
||||
integrity sha512-ZsWMI0j7EcFCMqbqdVwMWhmsVc03FhmypWXokKopGhwySn4IAB4AOURonRmFrO7k6sDeQ+iJ9QtTu7jA+S8wmg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-carousel@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-carousel/-/mjml-carousel-4.17.1.tgz#c395842741f55420dd7a3c08a76cd3d3a73e49ba"
|
||||
integrity sha512-pWo/aIgRL3XduckOBVEvbpph3vR4f9maRrbJ8Jfu4NVI6Ws3PQ6wt7HPXHmJlzJlK0gTiAF9f4+I076RVHPG7A==
|
||||
mjml-carousel@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-carousel/-/mjml-carousel-4.18.0.tgz#56e61a7873b91d9549d9992f517a471cdb433030"
|
||||
integrity sha512-wY4g1CHCOoVSZuar7CLFon/qkPbICu71IT+6pa4BDwkAiaAMAemZPyy+a+iIUgdc8kHgSuHGsGf6PQzBSMWRZA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-cli@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-cli/-/mjml-cli-4.17.1.tgz#0bc278762bc2391b6c61d16784156ee429cd104f"
|
||||
integrity sha512-1cMWP+yDDBUIjDYnjiKhIPW3NYJrt/W5rqOiB3zOTZQBT722Uh055S3BoLUikKxc+1sQPww4d9dH371zX2HaXA==
|
||||
mjml-cli@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-cli/-/mjml-cli-4.18.0.tgz#779902569f609ca6c1f26fadced10770a286b6fd"
|
||||
integrity sha512-N6CnA4o/q/VRnGPxTzvVnjAEcF7WUVVQGYfS9SPAp0qwyf7RysMmewdS9yN8GwXwZV6L2sKdn+3ANNi2FNsJ7w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
chokidar "^3.0.0"
|
||||
@@ -629,25 +629,25 @@ mjml-cli@4.17.1:
|
||||
js-beautify "^1.6.14"
|
||||
lodash "^4.17.21"
|
||||
minimatch "^9.0.3"
|
||||
mjml-core "4.17.1"
|
||||
mjml-migrate "4.17.1"
|
||||
mjml-parser-xml "4.17.1"
|
||||
mjml-validator "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
mjml-migrate "4.18.0"
|
||||
mjml-parser-xml "4.18.0"
|
||||
mjml-validator "4.18.0"
|
||||
yargs "^17.7.2"
|
||||
|
||||
mjml-column@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-column/-/mjml-column-4.17.1.tgz#3aa64972bb827fd96b596c71ce583e9ef720aa41"
|
||||
integrity sha512-S+oNZaWFP1/TCEgVwVcwqYIyHwwVZWSKLKj4fcWIMUCaHWKuojYrexOGfULDAwTjYEDhZaRDrrq96ulF12wJeQ==
|
||||
mjml-column@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-column/-/mjml-column-4.18.0.tgz#d8357449af5804e6eb506b8cf87691787e959cf5"
|
||||
integrity sha512-0QZ1whxbHUmJaRT8tW+wmr3fWZ/kpsHKAd24c7Z/N1Otm/U2G0T/FFEFJ6cB25X6ZN0K40QZ8L9gdLfiSVuRbA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-core@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-core/-/mjml-core-4.17.1.tgz#a303e8e4f94f5124284843bebd70c1d91a9085a6"
|
||||
integrity sha512-u2aHbBxFA2uJdS6T1A1ZTGYryPNebHMByRrMPCbe5W8Os+sGiC5gKLhZgjavZteKiMS+09swkvfneNLGYwzBKg==
|
||||
mjml-core@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-core/-/mjml-core-4.18.0.tgz#36d9ef9ff26cb010f81bef5342cf643b4fdf8043"
|
||||
integrity sha512-yey72LszXvIo5p0R6DB+YU8er/nP2wPsqpLKQCB0H8vG0WRT1sbSUvnCUOkKGn7subuyWDTdzHKbQO3XYIOmvg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
cheerio "1.0.0-rc.12"
|
||||
@@ -656,263 +656,263 @@ mjml-core@4.17.1:
|
||||
js-beautify "^1.6.14"
|
||||
juice "^10.0.0"
|
||||
lodash "^4.17.21"
|
||||
mjml-migrate "4.17.1"
|
||||
mjml-parser-xml "4.17.1"
|
||||
mjml-validator "4.17.1"
|
||||
mjml-migrate "4.18.0"
|
||||
mjml-parser-xml "4.18.0"
|
||||
mjml-validator "4.18.0"
|
||||
|
||||
mjml-divider@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-divider/-/mjml-divider-4.17.1.tgz#1219c4f25d9e6f963de9438a5adf85064c7e9f01"
|
||||
integrity sha512-KUWvcx1cIDwkN/gDuY37e9Vv+0U5U+xOVOfXRGloSnapYcP0IvmLtLTJeBwvGhwoN30wBiHDGwO8p/7B6CyxqQ==
|
||||
mjml-divider@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-divider/-/mjml-divider-4.18.0.tgz#36877b43836d7d05b190c109df5af73a781b6a80"
|
||||
integrity sha512-FmGUVJqi4RYroh7y85vDx0aUKZgECkxHtMQ4pkLGQbZ2g93/Qt0Ek88DVCNJ5XwUAQQkE/TvrGMLHp3CIqpQ9Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-group@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-group/-/mjml-group-4.17.1.tgz#38b4da7e67151c2c8c84378ef176432a135e11e2"
|
||||
integrity sha512-0vOcLm7l4ptLM5rqC6DhCafxIw5+WUrSYLcdUSJxO3AZMGJMxU7fkWeWGowE+PQdgqh6ee1/4RYc2qJDWtHW5A==
|
||||
mjml-group@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-group/-/mjml-group-4.18.0.tgz#09ec6c78e36c5c074260d888505efde645bc3da9"
|
||||
integrity sha512-28ABkXsKljBqj7XCC8GkQ94xz8HEU2XTyD+9LTlkDafzGp/MGJb8DcLh/7IkxCwqkQWyeMiDNLf1djsQ909Vxw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-head-attributes@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-attributes/-/mjml-head-attributes-4.17.1.tgz#198ca06a6a9b9148af1b6508aab38a41c2cf9b30"
|
||||
integrity sha512-p+g33eI4xyHb9Yv28zIXnNdsXQYvoGex/GvoGumwyxu6O8OOvPk1mIV87SjDISQHosJJMcTiZVd/RfBlwnZpGA==
|
||||
mjml-head-attributes@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-attributes/-/mjml-head-attributes-4.18.0.tgz#bf311924af6d2e0784df9bb95629bdc02583c0a7"
|
||||
integrity sha512-nLzix1wrMnojE0RPGhk4iKqSRwHKjie2EPzgKT7CDzfqN+Ref03E5Q19x3cQTLgxvq3C3CnvCQBfnhoS3Eakug==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-head-breakpoint@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-breakpoint/-/mjml-head-breakpoint-4.17.1.tgz#4fafa9cc176c052a2796d904ff84172290826adb"
|
||||
integrity sha512-vjsNAgdLnwqmkVlIENbH6odK6ZARiNQvsm+1o8CLo9ymw82WhIEbOnAeCfoddumZ6h2ywbZuBZzS23jJi13MUQ==
|
||||
mjml-head-breakpoint@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-breakpoint/-/mjml-head-breakpoint-4.18.0.tgz#4d9d2e35ec0bdb2fd2c530241c38df9c19106dfb"
|
||||
integrity sha512-k6rwff+7i+vTQYJ/CjBfE20qNqPaW60IRH2x2oEPuCzmwDmoVWOcplJIuotSqIAdfwF9hLkICknisp1BpczVlQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-head-font@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-font/-/mjml-head-font-4.17.1.tgz#0984862ebae07fbf2427e61c3d997ac155d56bd2"
|
||||
integrity sha512-Xeih/vqocR1BoBLbh8Sn67kNkfLsyHeZ7Z/3nyNz7TriZ//TGAR/PGTFFghQlXyX1BCtSx/eFoxMkKKswLYReA==
|
||||
mjml-head-font@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-font/-/mjml-head-font-4.18.0.tgz#f153a8b81275a3f25fa28142e3536e3dd4606c51"
|
||||
integrity sha512-ao8HB5nf+Dmxw4GO6lMMOlnj1lNZONai0GC9RobrZgPlghZw6hpURWGpkON7pQcy6XnOHwYwkV7Go/npzA2i7w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-head-html-attributes@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-html-attributes/-/mjml-head-html-attributes-4.17.1.tgz#cb4b74210257d9bb7ba9b23ca5cc409516456b8b"
|
||||
integrity sha512-O7YzEAFtSELB7wVYV808g6JcxXrzHk5glDdzzCEhDR4bjPHewSUpkrYOqvt0BdfdFsvqH4zm4vsJImUMW692HQ==
|
||||
mjml-head-html-attributes@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-html-attributes/-/mjml-head-html-attributes-4.18.0.tgz#167527f926919ff0cffdc8456e5db17953c81a11"
|
||||
integrity sha512-xaQE1rthe0RrNotwEr71X1tE+QQ489Yc0ynMm3oNMrohDI/TaCeazx8GAHPMM7VLduDA8D4A5wkZ6PuEvlJu4w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-head-preview@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-preview/-/mjml-head-preview-4.17.1.tgz#63e52bae35b43bdc43da838a0c3e85f3131439ef"
|
||||
integrity sha512-XL+8N9yrADJSw4gX9lvDcp31ghGy8WavenVO8UhxPyhLu/sMJ9lFXLbTB4z5JU1z4t/HPEp/GtgMGxAbr+QrQQ==
|
||||
mjml-head-preview@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-preview/-/mjml-head-preview-4.18.0.tgz#6469215ee85c15ffb58801d5aea3a9f1db337d2c"
|
||||
integrity sha512-2JvYqhbLyU/+Te6/1AXxzTNoHYCDYhXOVZP7wMvU4t7K34pXqyRUNO405atyHUY1MRafrl6RJ8cIx0x5vUX7PA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-head-style@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-style/-/mjml-head-style-4.17.1.tgz#29a1cb440ae36e0029af6cf1adc9eeb181ada09f"
|
||||
integrity sha512-YTjtqZAG0hD0aYwk02/8hS1W+T4nDUhVCBFmcxL/aTSrRbJQew0dSVtCvqNpAsbIJCUg/mUxx6pKKzRPdN+FtA==
|
||||
mjml-head-style@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-style/-/mjml-head-style-4.18.0.tgz#21746637547d8a2b9b1f9337349be4f1f95b3b20"
|
||||
integrity sha512-nEwDHkAqY3Fm7QWeAZc/a7MakZpXh6THfrE8/AWrfpgzTHrD/wihNUc09ztNpr6z/K1+JWgQfSF2BRc+X3P46g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-head-title@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-title/-/mjml-head-title-4.17.1.tgz#def969e9ac58e975bc686fc95273ddc2d23925f1"
|
||||
integrity sha512-cUO4b7tDuX1BLu6XYnPgG40o3pBUCkT+Yzu5DGsvRxvCWougJFN68ocF6zcc7OOanmLgBYlJevQKUyT6W5Rp0g==
|
||||
mjml-head-title@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-title/-/mjml-head-title-4.18.0.tgz#82f43cf4ca30f31756ce6fc4c7b71aba14d59041"
|
||||
integrity sha512-0Hm8o50rPMUQLSCOOa4D4pz9NajmCDccLvBYE4fwKdeUXjSJ6bwAYeMpveel8oNZMDUVJ4Hx+PskisEGHMHM2w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-head@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head/-/mjml-head-4.17.1.tgz#516d6039e103424d05ec5b55202b79a2b9a440f4"
|
||||
integrity sha512-+DBJ6UvkpYkKJGJKqo8luucDGbg9+rQZKytl/4VOGTE8bmbrKFixY3lkfmBrSkQ7/t6L4dDVSXywl6H91JsL+g==
|
||||
mjml-head@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head/-/mjml-head-4.18.0.tgz#93dd1194fea0f51f23454f24835e7b3428b40872"
|
||||
integrity sha512-DS0adpIAsVMDIk2DOsHzjg+RNjQU0fF8jiVP9BmdRHVGrLPmpL9wIHZk2KvsKvZe7VaXXBijFt3DZ5/CQ/+D7Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-hero@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-hero/-/mjml-hero-4.17.1.tgz#d4f7ad9e29cb11107843f68a906f9389acb6a230"
|
||||
integrity sha512-WDmNVJ4+xHLrkYOrGrq23hUYDVG3iFSyk/vIC/KlcG5Kebu5vVWbe6n3ZEucatPuYn/EUVV1ofIJM6dnXXfkGQ==
|
||||
mjml-hero@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-hero/-/mjml-hero-4.18.0.tgz#13f6cb1763c3cdbafbbf5b7b1cbd6e4d76e25f61"
|
||||
integrity sha512-rujm0ROM4QGWw77vnl3NaVaCKXrT4xTSHeAnkHKiY5AuRf6HPTgEtutq5pdel/y6Q9GrmxvN3HRESum7tpJCJw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-image@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-image/-/mjml-image-4.17.1.tgz#9a427d719caf664b3a60b8f6cfb10e91dabdcb5d"
|
||||
integrity sha512-ZIFXmP2Fb77vvX8SBQYbrAPPvkqx5GqJ7AqVWteQk4iz6nJf8GspZiotWyL4LvgZzf0B81aQCB11y7+RvAfVvw==
|
||||
mjml-image@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-image/-/mjml-image-4.18.0.tgz#c909279b7742c8e75e2b146b8f1c565e1a0ad503"
|
||||
integrity sha512-e09NkoYwvzMcTv7V6H5doWD6Te2E1y2EvOLQJoXKVdQpDwyBWGdfnZke0scJGdA58HLAB+0mLYogpLwmfLaP5Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-migrate@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-migrate/-/mjml-migrate-4.17.1.tgz#d50dd85f5f964d2e860741e657da03078209eeb4"
|
||||
integrity sha512-Rb66BdvuV8fGYdQJzvLK0naWGI8G9smzm1OJDjdhcCrQU3BfTW/BiTS9FP5G0W73kFJe//vlHCDZ3uBIr6REAA==
|
||||
mjml-migrate@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-migrate/-/mjml-migrate-4.18.0.tgz#1b08c93a9358b45f5b607043872fad216ef0ff9e"
|
||||
integrity sha512-qfNCgW9zhJIsbPyXFA5RT/WY4mlje3N0WhHHOsHc0nY89Q01DenyslUy9nLLGXwi4K5FHS58oCjwWbMhwDcj1w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
js-beautify "^1.6.14"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-parser-xml "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
mjml-parser-xml "4.18.0"
|
||||
yargs "^17.7.2"
|
||||
|
||||
mjml-navbar@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-navbar/-/mjml-navbar-4.17.1.tgz#215e1dc8546dc9658af59770113ac0f9b3eae47e"
|
||||
integrity sha512-SWtovALlb+tM2lu2stlsKItrM/Tc/YxWiCm+UtLuOvkBmouBX/vASufaFab3VPAq/pGJKF9nFGX2eWoJCGA4rA==
|
||||
mjml-navbar@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-navbar/-/mjml-navbar-4.18.0.tgz#b367eac34f22aa05e5506c07db6a0273b6255c67"
|
||||
integrity sha512-uho/MS2tfNAe+V9u2X7NoCco34MDbdp30ETA8009Qo1VCP/D8lZ+s69WGRPu6hvN/Y2pzBgZly++CMg3qFZqBQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-parser-xml@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-parser-xml/-/mjml-parser-xml-4.17.1.tgz#7b497c20bf1bb343fe49e2c79b24aa5ae926a4a8"
|
||||
integrity sha512-8cc1+cI1+ymeKmiaioZMaIzg8K9SmCErr0WOdS0n90pnt5eLqGQEh3RQJv7VoucO5aoJXgAnCSGeCstVXvZykg==
|
||||
mjml-parser-xml@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-parser-xml/-/mjml-parser-xml-4.18.0.tgz#53f78f6d5710eee9fcf4bf110d19cd4b300d66bf"
|
||||
integrity sha512-sHSsZg4afY1heThuJzxa1Kvfh/QzB7/9P5fFUHeVnnxb07ZTXnhXWA6YbobdND5/l9+5yjN5/UgqDZm3tIT4Uw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
detect-node "2.1.0"
|
||||
htmlparser2 "^9.1.0"
|
||||
lodash "^4.17.21"
|
||||
|
||||
mjml-preset-core@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-preset-core/-/mjml-preset-core-4.17.1.tgz#7826184b7ca57383e47597c1593e492a9a5b4102"
|
||||
integrity sha512-cFfelKeRJNG+WZv+kGWjjHrQam5PiHH8JaC3vvjl1eEwLcR2nbaYArlnLTIzgG+M3+cBlIl0Ru3Say5ZqWAcxw==
|
||||
mjml-preset-core@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-preset-core/-/mjml-preset-core-4.18.0.tgz#183b79cf51847e898c6e0c70601328665e85e2ef"
|
||||
integrity sha512-x3l8vMVtsaqM/jauMeZIN7HFD2t5A28J4U0o4849yIlRxiWguLFV5l3BL8Byol+YLkoLuT9PjaZs9RYv+FGfeg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
mjml-accordion "4.17.1"
|
||||
mjml-body "4.17.1"
|
||||
mjml-button "4.17.1"
|
||||
mjml-carousel "4.17.1"
|
||||
mjml-column "4.17.1"
|
||||
mjml-divider "4.17.1"
|
||||
mjml-group "4.17.1"
|
||||
mjml-head "4.17.1"
|
||||
mjml-head-attributes "4.17.1"
|
||||
mjml-head-breakpoint "4.17.1"
|
||||
mjml-head-font "4.17.1"
|
||||
mjml-head-html-attributes "4.17.1"
|
||||
mjml-head-preview "4.17.1"
|
||||
mjml-head-style "4.17.1"
|
||||
mjml-head-title "4.17.1"
|
||||
mjml-hero "4.17.1"
|
||||
mjml-image "4.17.1"
|
||||
mjml-navbar "4.17.1"
|
||||
mjml-raw "4.17.1"
|
||||
mjml-section "4.17.1"
|
||||
mjml-social "4.17.1"
|
||||
mjml-spacer "4.17.1"
|
||||
mjml-table "4.17.1"
|
||||
mjml-text "4.17.1"
|
||||
mjml-wrapper "4.17.1"
|
||||
mjml-accordion "4.18.0"
|
||||
mjml-body "4.18.0"
|
||||
mjml-button "4.18.0"
|
||||
mjml-carousel "4.18.0"
|
||||
mjml-column "4.18.0"
|
||||
mjml-divider "4.18.0"
|
||||
mjml-group "4.18.0"
|
||||
mjml-head "4.18.0"
|
||||
mjml-head-attributes "4.18.0"
|
||||
mjml-head-breakpoint "4.18.0"
|
||||
mjml-head-font "4.18.0"
|
||||
mjml-head-html-attributes "4.18.0"
|
||||
mjml-head-preview "4.18.0"
|
||||
mjml-head-style "4.18.0"
|
||||
mjml-head-title "4.18.0"
|
||||
mjml-hero "4.18.0"
|
||||
mjml-image "4.18.0"
|
||||
mjml-navbar "4.18.0"
|
||||
mjml-raw "4.18.0"
|
||||
mjml-section "4.18.0"
|
||||
mjml-social "4.18.0"
|
||||
mjml-spacer "4.18.0"
|
||||
mjml-table "4.18.0"
|
||||
mjml-text "4.18.0"
|
||||
mjml-wrapper "4.18.0"
|
||||
|
||||
mjml-raw@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-raw/-/mjml-raw-4.17.1.tgz#0422013a4b8c6f35afdc624e56b47039c6c174f2"
|
||||
integrity sha512-CnfgXh+c8u/jOuVjmv9N6Hxal5U4PPJFVY1JFRRJr/7Tcxl8aJUF03mBjqW9zAzoYO1bRcgyG3clchyEwwXQ8g==
|
||||
mjml-raw@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-raw/-/mjml-raw-4.18.0.tgz#8683b716c592a12560175697407e5130373cf8ac"
|
||||
integrity sha512-F/kViAwXm3ccPP52kw++/mHQbcYbYYxC8JH15TZxH8GLVZkX5CGKgcBrHhDK7WoIlfEIsVRZ6IZdlHjH8vgyxw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-section@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-section/-/mjml-section-4.17.1.tgz#10339354719e7e2c02911e56510811fb5bf9fa5b"
|
||||
integrity sha512-YrkvcBgJw2NBnPirjuVU4AoqwySZzOovm5sfryID9I59EmmG+lbBJOnv/v/5wXQSlw2a4n1+VX2sCUcH5/O5sA==
|
||||
mjml-section@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-section/-/mjml-section-4.18.0.tgz#ca4257bdd83c6202b03ace84f7a6246f453f9669"
|
||||
integrity sha512-bB8My9zvIEkTOxej+TrjEeaeRT0lsypGeRADtdrRZXeqUClkkuCnCXlsNKSLGT8ZRqjUqWRc5z8ubDOvGk2+Gg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-social@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-social/-/mjml-social-4.17.1.tgz#52e9f28799a1992ae291b1b7000b7c2b58cb23be"
|
||||
integrity sha512-Agp6CHJn7SwD+cckCxibZ/32luTzAiDJDlKH0SjQ+9NvSoGskkhii3yOqtYnJ+t3NmQkxpRkXOnUN4GEbupghA==
|
||||
mjml-social@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-social/-/mjml-social-4.18.0.tgz#c9eeb1116f77120431fa27e583e3513852e0c487"
|
||||
integrity sha512-iAQc9g59L6L3VHDd55BxeIvk/zHkxflxmvuyYyOOvpmmKAvUBC//ULfpxiiM4yupofsThqFfrO+wc8d4kTRkbQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-spacer@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-spacer/-/mjml-spacer-4.17.1.tgz#5a81281872f3f2556c1828bf24e8df475ac71463"
|
||||
integrity sha512-TxXDosuRzuoQNdceG47TKy+NWbwIGZmVDV/4XRtkcPHEvlsHpIIzn2+zzj+xrA6qh5Z+zlXL+x8ZpWMqrUoKfQ==
|
||||
mjml-spacer@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-spacer/-/mjml-spacer-4.18.0.tgz#ab55416f943ca1b3e515bba6203d81b88eab8337"
|
||||
integrity sha512-FK/0f5IBiONgaRpwNBs7G8EbLdAbmYqcIfHR8O8tP4LipAChLQKHO9vX3vrRMGLBZZNTESLObcFSVWmA40Mfpw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-table@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-table/-/mjml-table-4.17.1.tgz#0955b75ff86eb80a511cbcd7a37befca8c41101c"
|
||||
integrity sha512-AcAcsNrpzTOsNc0X0i0+5+iNNGEnYjwn9qodF/413yuWDSH9p7SL8vFuI3Snmgv9s1dR+BKDiF8uPt4XTOMlzA==
|
||||
mjml-table@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-table/-/mjml-table-4.18.0.tgz#3c57741f3aff14428921157d63912709aa005adc"
|
||||
integrity sha512-vJysCPUL3CHcsQDAFpW+skzBtY0RYsmMBYswI4WX0B05GLKlOjXqpYOwcmAupWeGoBVL5r/t28ynu2PqnOlN3w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-text@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-text/-/mjml-text-4.17.1.tgz#77e1598c1e4d98c10490d242c9928ec3aa6e3663"
|
||||
integrity sha512-pOrz8tRU3hReKd+K69dJmiVndC0+gB5IfVKIK3fdvYMb9laZBAstkXW0j5wn/0Af4FZSlJkDRLM7Ylxbh1+fqQ==
|
||||
mjml-text@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-text/-/mjml-text-4.18.0.tgz#f4855611975643cef33bf1b334e1734956994d8e"
|
||||
integrity sha512-hBLmF3JgveUKktKQFWHqHAr7qr92j1CxAvq7mtpDUgiWgyPFzqRX8mUsFYgZ7DmRxG4UE+Kzpt8/YFd9+E98lw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
|
||||
mjml-validator@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-validator/-/mjml-validator-4.17.1.tgz#d73fb08bc368763f6bf0898a88b6b8452573b2d2"
|
||||
integrity sha512-0Au5L5fIfAzOJpQG4PkpFeV0mbzCgjCTu5XbG7pJX4Wup72TGYwrA6Aq2yAdlx17kFPWThSZxeB3Xpd3/kwqOg==
|
||||
mjml-validator@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-validator/-/mjml-validator-4.18.0.tgz#6f5fb9804a6bb5910aa9cf0b64896fb6f9ae0132"
|
||||
integrity sha512-JmpWAsNTUlAxJOz2zHYfF8Vod8OzM3Qp5JXtrVw5tivZQzq88ZfqVGuqsas51z0pp1/ilfD4lC17YGfGwKGyhA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
|
||||
mjml-wrapper@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml-wrapper/-/mjml-wrapper-4.17.1.tgz#8832cd7da08a32478189041b8b1c6538f204022a"
|
||||
integrity sha512-c0bCgXCwffI4krnQYU0Zp8ifGkYMgE7a65NAWXlV3AWEfVmjDlhCcD8LBfZ8UfY8zR3Che8pnunowPZfwh0Nxg==
|
||||
mjml-wrapper@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-wrapper/-/mjml-wrapper-4.18.0.tgz#73e62fa05f307c6b763233284e8f27a16c3c03cd"
|
||||
integrity sha512-TZeOvLjIhXEK60rjWNiYhEYNlv5GKYahE+96ifcT5OGkWkRA0DsQDfp+6VI32OS5VxsfKq2h/UdERPlQijjpAQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.17.1"
|
||||
mjml-section "4.17.1"
|
||||
mjml-core "4.18.0"
|
||||
mjml-section "4.18.0"
|
||||
|
||||
mjml@4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mjml/-/mjml-4.17.1.tgz#fe77de5258f31b42532f601ccb10058a2d95bdee"
|
||||
integrity sha512-aqy5EVZuwXIINl+d7vC1Fn+MzMfIU4qxCx2TUHnGJxYONrtNIgSQEDlgB2ns2oK8a8WgPuEJCZBYwRE+5ZFcng==
|
||||
mjml@4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml/-/mjml-4.18.0.tgz#9e4b37d26c1ad689ca26f889048b884e47e26fff"
|
||||
integrity sha512-rQM4aqFRrNvV1k733e8hJSopBjZvoSdBpRYzNTMAN+As0jqJsO5eN0wTT2IFtfe4PREzzu5b06RkPiUQdd0IIg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
mjml-cli "4.17.1"
|
||||
mjml-core "4.17.1"
|
||||
mjml-migrate "4.17.1"
|
||||
mjml-preset-core "4.17.1"
|
||||
mjml-validator "4.17.1"
|
||||
mjml-cli "4.18.0"
|
||||
mjml-core "4.18.0"
|
||||
mjml-migrate "4.18.0"
|
||||
mjml-preset-core "4.18.0"
|
||||
mjml-validator "4.18.0"
|
||||
|
||||
no-case@^2.2.0:
|
||||
version "2.3.2"
|
||||
|
||||
Reference in New Issue
Block a user