mirror of
https://github.com/suitenumerique/docs.git
synced 2026-05-06 23:22:15 +02:00
Compare commits
1 Commits
fix/access
...
sbl-public
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
270b374a17 |
54
CHANGELOG.md
54
CHANGELOG.md
@@ -6,52 +6,6 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) integrate configurable Waffle #1795
|
||||
|
||||
### Fixed
|
||||
|
||||
- ✅(e2e) fix e2e test for other browsers #1799
|
||||
- 🐛(frontend) add fallback for unsupported Blocknote languages #1810
|
||||
- 🐛(frontend) fix emojipicker closing in tree #1808
|
||||
|
||||
### Changed
|
||||
|
||||
- ♿(frontend) improve accessibility:
|
||||
- ♿️(frontend) fix subdoc opening and emoji pick focus #1745
|
||||
- ♿️(frontend) Keyboard focus Fixes for docs Tree/Editor #1816
|
||||
|
||||
## [4.4.0] - 2026-01-13
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(backend) add documents/all endpoint with descendants #1553
|
||||
- ✅(export) add PDF regression tests #1762
|
||||
- 📝(docs) Add language configuration documentation #1757
|
||||
- 🔒(helm) Set default security context #1750
|
||||
- ✨(backend) use langfuse to monitor AI actions #1776
|
||||
|
||||
### Changed
|
||||
|
||||
- ♿(frontend) improve accessibility:
|
||||
- ♿(frontend) make html export accessible to screen reader users #1743
|
||||
- ♿(frontend) add missing label and fix Axes errors to improve a11y #1693
|
||||
|
||||
### Fixed
|
||||
|
||||
- ✅(backend) reduce flakiness on backend test #1769
|
||||
- 🐛(frontend) fix clickable main content regression #1773
|
||||
- 🐛(backend) fix TRASHBIN_CUTOFF_DAYS type error #1778
|
||||
- 💄(frontend) fix icon position in callout block #1779
|
||||
|
||||
### Security
|
||||
|
||||
- 🔒️(backend) validate more strictly url used by cors-proxy endpoint #1768
|
||||
- 🔒️(frontend) fix props vulnerability in Interlinking #1792
|
||||
|
||||
## [4.3.0] - 2026-01-05
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(helm) redirecting system #1697
|
||||
@@ -66,8 +20,9 @@ and this project adheres to
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) fix tables deletion #1739
|
||||
- 🐛(frontend) fix tables deletion #1752
|
||||
- 🐛(frontend) fix children not display when first resize #1753
|
||||
- 📝(doc) fix publiccode.yml syntax #1770
|
||||
|
||||
## [4.2.0] - 2025-12-17
|
||||
|
||||
@@ -93,6 +48,7 @@ and this project adheres to
|
||||
- 🐛(frontend) Select text + Go back one page crash the app #1733
|
||||
- 🐛(frontend) fix versioning conflict #1742
|
||||
|
||||
|
||||
## [4.1.0] - 2025-12-09
|
||||
|
||||
### Added
|
||||
@@ -1006,9 +962,7 @@ 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.4.0...main
|
||||
[v4.4.0]: https://github.com/suitenumerique/docs/releases/v4.4.0
|
||||
[v4.3.0]: https://github.com/suitenumerique/docs/releases/v4.3.0
|
||||
[unreleased]: https://github.com/suitenumerique/docs/compare/v4.2.0...main
|
||||
[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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB |
@@ -64,9 +64,6 @@ These are the environment variables you can set for the `impress-backend` contai
|
||||
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | Frontend feature flag to display the homepage | false |
|
||||
| FRONTEND_THEME | Frontend theme to use | |
|
||||
| LANGUAGE_CODE | Default language | en-us |
|
||||
| LANGFUSE_SECRET_KEY | The Langfuse secret key used by the sdk | None |
|
||||
| LANGFUSE_PUBLIC_KEY | The Langfuse public key used by the sdk | None |
|
||||
| LANGFUSE_BASE_URL | The Langfuse base url used by the sdk | None |
|
||||
| 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 |
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
# Language Configuration (2025-12)
|
||||
|
||||
This document explains how to configure and override the available languages in the Docs application.
|
||||
|
||||
## Default Languages
|
||||
|
||||
By default, the application supports the following languages (in priority order):
|
||||
|
||||
- English (en-us)
|
||||
- French (fr-fr)
|
||||
- German (de-de)
|
||||
- Dutch (nl-nl)
|
||||
- Spanish (es-es)
|
||||
|
||||
The default configuration is defined in `src/backend/impress/settings.py`:
|
||||
|
||||
```python
|
||||
LANGUAGES = values.SingleNestedTupleValue(
|
||||
(
|
||||
("en-us", "English"),
|
||||
("fr-fr", "Français"),
|
||||
("de-de", "Deutsch"),
|
||||
("nl-nl", "Nederlands"),
|
||||
("es-es", "Español"),
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Overriding Languages
|
||||
|
||||
### Using Environment Variables
|
||||
|
||||
You can override the available languages by setting the `DJANGO_LANGUAGES` environment variable. This is the recommended approach for customizing language support without modifying the source code.
|
||||
|
||||
#### Format
|
||||
|
||||
The `DJANGO_LANGUAGES` variable expects a semicolon-separated list of language configurations, where each language is defined as `code,Display Name`:
|
||||
|
||||
```
|
||||
DJANGO_LANGUAGES=code1,Name1;code2,Name2;code3,Name3
|
||||
```
|
||||
|
||||
#### Example Configurations
|
||||
|
||||
**Example 1: English and French only**
|
||||
|
||||
```bash
|
||||
DJANGO_LANGUAGES=en-us,English;fr-fr,Français
|
||||
```
|
||||
|
||||
**Example 2: Add Italian and Chinese**
|
||||
|
||||
```bash
|
||||
DJANGO_LANGUAGES=en-us,English;fr-fr,Français;de-de,Deutsch;it-it,Italiano;zh-cn,中文
|
||||
```
|
||||
|
||||
**Example 3: Custom subset of languages**
|
||||
|
||||
```bash
|
||||
DJANGO_LANGUAGES=fr-fr,Français;de-de,Deutsch;es-es,Español
|
||||
```
|
||||
|
||||
### Configuration Files
|
||||
|
||||
#### Development Environment
|
||||
|
||||
For local development, you can set the `DJANGO_LANGUAGES` variable in your environment configuration file:
|
||||
|
||||
**File:** `env.d/development/common.local`
|
||||
|
||||
```bash
|
||||
DJANGO_LANGUAGES=en-us,English;fr-fr,Français;de-de,Deutsch;it-it,Italiano;zh-cn,中文;
|
||||
```
|
||||
|
||||
#### Production Environment
|
||||
|
||||
For production deployments, add the variable to your production environment configuration:
|
||||
|
||||
**File:** `env.d/production.dist/common`
|
||||
|
||||
```bash
|
||||
DJANGO_LANGUAGES=en-us,English;fr-fr,Français
|
||||
```
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
When using Docker Compose, you can set the environment variable in your `compose.yml` or `compose.override.yml` file:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
environment:
|
||||
- DJANGO_LANGUAGES=en-us,English;fr-fr,Français;de-de,Deutsch
|
||||
```
|
||||
|
||||
## Important Considerations
|
||||
|
||||
### Language Codes
|
||||
|
||||
- Use standard language codes (ISO 639-1 with optional region codes)
|
||||
- Format: `language-region` (e.g., `en-us`, `fr-fr`, `de-de`)
|
||||
- Use lowercase for language codes and region identifiers
|
||||
|
||||
### Priority Order
|
||||
|
||||
Languages are listed in priority order. The first language in the list is used as the fallback language throughout the application when a specific translation is not available.
|
||||
|
||||
### Translation Availability
|
||||
|
||||
Before adding a new language, ensure that:
|
||||
|
||||
1. Translation files exist for that language in the `src/backend/locale/` directory
|
||||
2. The frontend application has corresponding translation files
|
||||
3. All required messages have been translated
|
||||
|
||||
#### Available Languages
|
||||
|
||||
The following languages have translation files available in `src/backend/locale/`:
|
||||
|
||||
- `br_FR` - Breton (France)
|
||||
- `cn_CN` - Chinese (China) - *Note: Use `zh-cn` in DJANGO_LANGUAGES*
|
||||
- `de_DE` - German (Germany) - Use `de-de`
|
||||
- `en_US` - English (United States) - Use `en-us`
|
||||
- `es_ES` - Spanish (Spain) - Use `es-es`
|
||||
- `fr_FR` - French (France) - Use `fr-fr`
|
||||
- `it_IT` - Italian (Italy) - Use `it-it`
|
||||
- `nl_NL` - Dutch (Netherlands) - Use `nl-nl`
|
||||
- `pt_PT` - Portuguese (Portugal) - Use `pt-pt`
|
||||
- `ru_RU` - Russian (Russia) - Use `ru-ru`
|
||||
- `sl_SI` - Slovenian (Slovenia) - Use `sl-si`
|
||||
- `sv_SE` - Swedish (Sweden) - Use `sv-se`
|
||||
- `tr_TR` - Turkish (Turkey) - Use `tr-tr`
|
||||
- `uk_UA` - Ukrainian (Ukraine) - Use `uk-ua`
|
||||
- `zh_CN` - Chinese (China) - Use `zh-cn`
|
||||
|
||||
**Note:** When configuring `DJANGO_LANGUAGES`, use lowercase with hyphens (e.g., `pt-pt`, `ru-ru`) rather than the directory name format.
|
||||
|
||||
### Translation Management
|
||||
|
||||
We use [Crowdin](https://crowdin.com/) to manage translations for the Docs application. Crowdin allows our community to contribute translations and helps maintain consistency across all supported languages.
|
||||
|
||||
**Want to add a new language or improve existing translations?**
|
||||
|
||||
If you would like us to support a new language or want to contribute to translations, please get in touch with the project maintainers. We can add new languages to our Crowdin project and coordinate translation efforts with the community.
|
||||
|
||||
### Cookie and Session
|
||||
|
||||
The application stores the user's language preference in a cookie named `docs_language`. The cookie path is set to `/` by default.
|
||||
|
||||
## Testing Language Configuration
|
||||
|
||||
After changing the language configuration:
|
||||
|
||||
1. Restart the application services
|
||||
2. Verify the language selector displays the correct languages
|
||||
3. Test switching between different languages
|
||||
4. Confirm that content is displayed in the selected language
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Languages not appearing
|
||||
|
||||
- Verify the environment variable is correctly formatted (semicolon-separated, comma between code and name)
|
||||
- Check that there are no trailing spaces in language codes or names
|
||||
- Ensure the application was restarted after changing the configuration
|
||||
|
||||
### Missing translations
|
||||
|
||||
If you add a new language but see untranslated text:
|
||||
|
||||
1. Check if translation files exist in `src/backend/locale/<language_code>/LC_MESSAGES/`
|
||||
2. Run Django's `makemessages` and `compilemessages` commands to generate/update translations
|
||||
3. Verify frontend translation files are available
|
||||
|
||||
## Related Configuration
|
||||
|
||||
- `LANGUAGE_CODE`: Default language code (default: `en-us`)
|
||||
- `LANGUAGE_COOKIE_NAME`: Cookie name for storing user language preference (default: `docs_language`)
|
||||
- `LANGUAGE_COOKIE_PATH`: Cookie path (default: `/`)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
# Customization Guide 🛠 ️
|
||||
|
||||
## Runtime Theming 🎨
|
||||
# Runtime Theming 🎨
|
||||
|
||||
### How to Use
|
||||
|
||||
@@ -34,7 +32,7 @@ Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom
|
||||
|
||||
----
|
||||
|
||||
## Runtime JavaScript Injection 🚀
|
||||
# Runtime JavaScript Injection 🚀
|
||||
|
||||
### How to Use
|
||||
|
||||
@@ -89,7 +87,7 @@ Then, set the `FRONTEND_JS_URL` environment variable to the URL of your custom J
|
||||
|
||||
----
|
||||
|
||||
## **Your Docs icon** 📝
|
||||
# **Your Docs icon** 📝
|
||||
|
||||
You can add your own Docs icon in the header from the theme customization file.
|
||||
|
||||
@@ -107,7 +105,7 @@ This configuration is optional. If not set, the default icon will be used.
|
||||
|
||||
----
|
||||
|
||||
## **Footer Configuration** 📝
|
||||
# **Footer Configuration** 📝
|
||||
|
||||
The footer is configurable from the theme customization file.
|
||||
|
||||
@@ -130,7 +128,7 @@ Below is a visual example of a configured footer ⬇️:
|
||||
|
||||
----
|
||||
|
||||
## **Custom Translations** 📝
|
||||
# **Custom Translations** 📝
|
||||
|
||||
The translations can be partially overridden from the theme customization file.
|
||||
|
||||
@@ -142,36 +140,4 @@ THEME_CUSTOMIZATION_FILE_PATH=<path>
|
||||
|
||||
### Example of JSON
|
||||
|
||||
The json must follow some rules: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
|
||||
|
||||
----
|
||||
|
||||
## **Waffle Configuration** 🧇
|
||||
|
||||
The Waffle (La Gaufre) is a widget that displays a grid of services.
|
||||
|
||||

|
||||
|
||||
### Settings 🔧
|
||||
|
||||
```shellscript
|
||||
THEME_CUSTOMIZATION_FILE_PATH=<path>
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
The Waffle can be configured in the theme customization file with the `waffle` key.
|
||||
|
||||
### Available Properties
|
||||
|
||||
See: [LaGaufreV2Props](https://github.com/suitenumerique/ui-kit/blob/main/src/components/la-gaufre/LaGaufreV2.tsx#L49)
|
||||
|
||||
### Complete Example
|
||||
|
||||
From the theme customization file: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
|
||||
|
||||
### Behavior
|
||||
|
||||
- If `data.services` is provided, the Waffle will display those services statically
|
||||
- If no data is provided, services can be fetched dynamically from an API endpoint thanks to the `apiUrl` property
|
||||
|
||||
The json must follow some rules: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
|
||||
@@ -1,16 +1,18 @@
|
||||
publiccodeYmlVersion: "2.4.0"
|
||||
publiccodeYmlVersion: "0.5.0"
|
||||
name: Docs
|
||||
url: https://github.com/suitenumerique/docs
|
||||
landingURL: https://github.com/suitenumerique/docs
|
||||
creationDate: 2023-12-10
|
||||
logo: https://raw.githubusercontent.com/suitenumerique/docs/main/docs/assets/docs-logo.png
|
||||
usedBy:
|
||||
- Direction interministériel du numérique (DINUM)
|
||||
- Direction interministérielle du numérique (DINUM)
|
||||
fundedBy:
|
||||
- name: Direction interministériel du numérique (DINUM)
|
||||
url: https://www.numerique.gouv.fr
|
||||
- name: Direction interministérielle du numérique (DINUM)
|
||||
uri: https://www.numerique.gouv.fr
|
||||
roadmap: "https://github.com/orgs/suitenumerique/projects/2/views/1"
|
||||
softwareType: "standalone/other"
|
||||
platforms:
|
||||
- "web"
|
||||
developmentStatus: "stable"
|
||||
description:
|
||||
en:
|
||||
shortDescription: "The open source document editor where your notes can become knowledge through live collaboration"
|
||||
@@ -18,10 +20,18 @@ description:
|
||||
shortDescription: "L'éditeur de documents open source où vos notes peuvent devenir des connaissances grâce à la collaboration en direct."
|
||||
legal:
|
||||
license: MIT
|
||||
localisation:
|
||||
localisationReady: true
|
||||
availableLanguages:
|
||||
- de
|
||||
- en
|
||||
- es
|
||||
- fr
|
||||
- nl
|
||||
maintenance:
|
||||
type: internal
|
||||
contacts:
|
||||
- name: "Virgile Deville"
|
||||
email: "virgile.deville@numerique.gouv.fr"
|
||||
- name: "samuel.paccoud"
|
||||
- name: "Samuel Paccoud"
|
||||
email: "samuel.paccoud@numerique.gouv.fr"
|
||||
|
||||
@@ -25,19 +25,6 @@
|
||||
"matchPackageNames": ["pylint"],
|
||||
"allowedVersions": "<4.0.0"
|
||||
},
|
||||
{
|
||||
"groupName": "allowed django versions",
|
||||
"matchManagers": ["pep621"],
|
||||
"matchPackageNames": ["django"],
|
||||
"allowedVersions": "<6.0.0"
|
||||
},
|
||||
{
|
||||
|
||||
"groupName": "allowed celery versions",
|
||||
"matchManagers": ["pep621"],
|
||||
"matchPackageNames": ["celery"],
|
||||
"allowedVersions": "<5.6.0"
|
||||
},
|
||||
{
|
||||
"enabled": false,
|
||||
"groupName": "ignored js dependencies",
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import base64
|
||||
import ipaddress
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from urllib.parse import unquote, urlencode, urlparse
|
||||
@@ -390,7 +388,6 @@ 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
|
||||
@@ -861,60 +858,6 @@ 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"],
|
||||
@@ -1657,101 +1600,6 @@ class DocumentViewSet(
|
||||
|
||||
return drf.response.Response(response, status=drf.status.HTTP_200_OK)
|
||||
|
||||
def _reject_invalid_ips(self, ips):
|
||||
"""
|
||||
Check if an IP address is safe from SSRF attacks.
|
||||
|
||||
Raises:
|
||||
drf.exceptions.ValidationError: If the IP is unsafe
|
||||
"""
|
||||
for ip in ips:
|
||||
# Block loopback addresses (check before private,
|
||||
# as 127.0.0.1 might be considered private)
|
||||
if ip.is_loopback:
|
||||
raise drf.exceptions.ValidationError(
|
||||
"Access to loopback addresses is not allowed"
|
||||
)
|
||||
|
||||
# Block link-local addresses (169.254.0.0/16) - check before private
|
||||
if ip.is_link_local:
|
||||
raise drf.exceptions.ValidationError(
|
||||
"Access to link-local addresses is not allowed"
|
||||
)
|
||||
|
||||
# Block private IP ranges
|
||||
if ip.is_private:
|
||||
raise drf.exceptions.ValidationError(
|
||||
"Access to private IP addresses is not allowed"
|
||||
)
|
||||
|
||||
# Block multicast addresses
|
||||
if ip.is_multicast:
|
||||
raise drf.exceptions.ValidationError(
|
||||
"Access to multicast addresses is not allowed"
|
||||
)
|
||||
|
||||
# Block reserved addresses (including 0.0.0.0)
|
||||
if ip.is_reserved:
|
||||
raise drf.exceptions.ValidationError(
|
||||
"Access to reserved IP addresses is not allowed"
|
||||
)
|
||||
|
||||
def _validate_url_against_ssrf(self, url):
|
||||
"""
|
||||
Validate that a URL is safe from SSRF (Server-Side Request Forgery) attacks.
|
||||
|
||||
Blocks:
|
||||
- localhost and its variations
|
||||
- Private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
|
||||
- Link-local addresses (169.254.0.0/16)
|
||||
- Loopback addresses
|
||||
|
||||
Raises:
|
||||
drf.exceptions.ValidationError: If the URL is unsafe
|
||||
"""
|
||||
parsed = urlparse(url)
|
||||
hostname = parsed.hostname
|
||||
|
||||
if not hostname:
|
||||
raise drf.exceptions.ValidationError("Invalid hostname")
|
||||
|
||||
# Resolve hostname to IP address(es)
|
||||
# Check all resolved IPs to prevent DNS rebinding attacks
|
||||
try:
|
||||
# Try to parse as IP address first (if hostname is already an IP)
|
||||
try:
|
||||
ip = ipaddress.ip_address(hostname)
|
||||
resolved_ips = [ip]
|
||||
except ValueError:
|
||||
# Resolve hostname to IP addresses (supports both IPv4 and IPv6)
|
||||
resolved_ips = []
|
||||
try:
|
||||
# Get all address info (IPv4 and IPv6)
|
||||
addr_info = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC)
|
||||
for family, _, _, _, sockaddr in addr_info:
|
||||
if family == socket.AF_INET:
|
||||
# IPv4
|
||||
ip = ipaddress.ip_address(sockaddr[0])
|
||||
resolved_ips.append(ip)
|
||||
elif family == socket.AF_INET6:
|
||||
# IPv6
|
||||
ip = ipaddress.ip_address(sockaddr[0])
|
||||
resolved_ips.append(ip)
|
||||
except (socket.gaierror, OSError) as e:
|
||||
raise drf.exceptions.ValidationError(
|
||||
f"Failed to resolve hostname: {str(e)}"
|
||||
) from e
|
||||
|
||||
if not resolved_ips:
|
||||
raise drf.exceptions.ValidationError(
|
||||
"No IP addresses found for hostname"
|
||||
) from None
|
||||
except ValueError as e:
|
||||
raise drf.exceptions.ValidationError(f"Invalid IP address: {str(e)}") from e
|
||||
|
||||
# Check all resolved IPs to ensure none are private/internal
|
||||
self._reject_invalid_ips(resolved_ips)
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=True,
|
||||
methods=["get"],
|
||||
@@ -1785,16 +1633,6 @@ class DocumentViewSet(
|
||||
status=drf.status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Validate URL against SSRF attacks
|
||||
try:
|
||||
self._validate_url_against_ssrf(url)
|
||||
except drf.exceptions.ValidationError as e:
|
||||
logger.error("Potential SSRF attack detected: %s", e)
|
||||
return drf.response.Response(
|
||||
{"detail": "Invalid URL used."},
|
||||
status=drf.status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
url,
|
||||
@@ -1803,15 +1641,13 @@ class DocumentViewSet(
|
||||
"User-Agent": request.headers.get("User-Agent", ""),
|
||||
"Accept": request.headers.get("Accept", ""),
|
||||
},
|
||||
allow_redirects=False,
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
content_type = response.headers.get("Content-Type", "")
|
||||
|
||||
if not content_type.startswith("image/"):
|
||||
return drf.response.Response(
|
||||
{"detail": "Invalid URL used."}, status=status.HTTP_400_BAD_REQUEST
|
||||
status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
||||
)
|
||||
|
||||
# Use StreamingHttpResponse with the response's iter_content to properly stream the data
|
||||
@@ -1829,7 +1665,7 @@ class DocumentViewSet(
|
||||
except requests.RequestException as e:
|
||||
logger.exception(e)
|
||||
return drf.response.Response(
|
||||
{"detail": "Invalid URL used."},
|
||||
{"error": f"Failed to fetch resource from {url}"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
@@ -3,14 +3,10 @@
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
from core import enums
|
||||
|
||||
if settings.LANGFUSE_PUBLIC_KEY:
|
||||
from langfuse.openai import OpenAI
|
||||
else:
|
||||
from openai import OpenAI
|
||||
|
||||
|
||||
AI_ACTIONS = {
|
||||
"prompt": (
|
||||
"Answer the prompt using markdown formatting for structure and emphasis. "
|
||||
|
||||
@@ -5,6 +5,7 @@ import re
|
||||
from unittest import mock
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
@@ -322,6 +323,85 @@ def test_authentication_getter_new_user_with_email(monkeypatch):
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
@override_settings(OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo")
|
||||
@responses.activate
|
||||
def test_authentication_get_userinfo_json_response():
|
||||
"""Test get_userinfo method with a JSON response."""
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(r".*/userinfo"),
|
||||
json={
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"email": "john.doe@example.com",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
oidc_backend = OIDCAuthenticationBackend()
|
||||
result = oidc_backend.get_userinfo("fake_access_token", None, None)
|
||||
|
||||
assert result["first_name"] == "John"
|
||||
assert result["last_name"] == "Doe"
|
||||
assert result["email"] == "john.doe@example.com"
|
||||
|
||||
|
||||
@override_settings(OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo")
|
||||
@responses.activate
|
||||
def test_authentication_get_userinfo_token_response(monkeypatch, settings):
|
||||
"""Test get_userinfo method with a token response."""
|
||||
settings.OIDC_RP_SIGN_ALGO = "HS256" # disable JWKS URL call
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(r".*/userinfo"),
|
||||
body="fake.jwt.token",
|
||||
status=200,
|
||||
content_type="application/jwt",
|
||||
)
|
||||
|
||||
def mock_verify_token(self, token): # pylint: disable=unused-argument
|
||||
return {
|
||||
"first_name": "Jane",
|
||||
"last_name": "Doe",
|
||||
"email": "jane.doe@example.com",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "verify_token", mock_verify_token)
|
||||
|
||||
oidc_backend = OIDCAuthenticationBackend()
|
||||
result = oidc_backend.get_userinfo("fake_access_token", None, None)
|
||||
|
||||
assert result["first_name"] == "Jane"
|
||||
assert result["last_name"] == "Doe"
|
||||
assert result["email"] == "jane.doe@example.com"
|
||||
|
||||
|
||||
@override_settings(OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo")
|
||||
@responses.activate
|
||||
def test_authentication_get_userinfo_invalid_response(settings):
|
||||
"""
|
||||
Test get_userinfo method with an invalid JWT response that
|
||||
causes verify_token to raise an error.
|
||||
"""
|
||||
settings.OIDC_RP_SIGN_ALGO = "HS256" # disable JWKS URL call
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(r".*/userinfo"),
|
||||
body="fake.jwt.token",
|
||||
status=200,
|
||||
content_type="application/jwt",
|
||||
)
|
||||
|
||||
oidc_backend = OIDCAuthenticationBackend()
|
||||
|
||||
with pytest.raises(
|
||||
SuspiciousOperation,
|
||||
match="User info response was not valid JWT",
|
||||
):
|
||||
oidc_backend.get_userinfo("fake_access_token", None, None)
|
||||
|
||||
|
||||
def test_authentication_getter_existing_disabled_user_via_sub(
|
||||
django_assert_num_queries, monkeypatch
|
||||
):
|
||||
|
||||
@@ -1,427 +0,0 @@
|
||||
"""
|
||||
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)}
|
||||
@@ -1,8 +1,5 @@
|
||||
"""Test on the CORS proxy API for documents."""
|
||||
|
||||
import socket
|
||||
import unittest.mock
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from requests.exceptions import RequestException
|
||||
@@ -13,17 +10,11 @@ from core import factories
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@unittest.mock.patch("core.api.viewsets.socket.getaddrinfo")
|
||||
@responses.activate
|
||||
def test_api_docs_cors_proxy_valid_url(mock_getaddrinfo):
|
||||
def test_api_docs_cors_proxy_valid_url():
|
||||
"""Test the CORS proxy API for documents with a valid URL."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
|
||||
# Mock DNS resolution to return a public IP address
|
||||
mock_getaddrinfo.return_value = [
|
||||
(socket.AF_INET, socket.SOCK_STREAM, 0, "", ("8.8.8.8", 0))
|
||||
]
|
||||
|
||||
client = APIClient()
|
||||
url_to_fetch = "https://external-url.com/assets/logo-gouv.png"
|
||||
responses.get(url_to_fetch, body=b"", status=200, content_type="image/png")
|
||||
@@ -65,17 +56,11 @@ def test_api_docs_cors_proxy_without_url_query_string():
|
||||
assert response.json() == {"detail": "Missing 'url' query parameter"}
|
||||
|
||||
|
||||
@unittest.mock.patch("core.api.viewsets.socket.getaddrinfo")
|
||||
@responses.activate
|
||||
def test_api_docs_cors_proxy_anonymous_document_not_public(mock_getaddrinfo):
|
||||
def test_api_docs_cors_proxy_anonymous_document_not_public():
|
||||
"""Test the CORS proxy API for documents with an anonymous user and a non-public document."""
|
||||
document = factories.DocumentFactory(link_reach="authenticated")
|
||||
|
||||
# Mock DNS resolution to return a public IP address
|
||||
mock_getaddrinfo.return_value = [
|
||||
(socket.AF_INET, socket.SOCK_STREAM, 0, "", ("8.8.8.8", 0))
|
||||
]
|
||||
|
||||
client = APIClient()
|
||||
url_to_fetch = "https://external-url.com/assets/logo-gouv.png"
|
||||
responses.get(url_to_fetch, body=b"", status=200, content_type="image/png")
|
||||
@@ -88,22 +73,14 @@ def test_api_docs_cors_proxy_anonymous_document_not_public(mock_getaddrinfo):
|
||||
}
|
||||
|
||||
|
||||
@unittest.mock.patch("core.api.viewsets.socket.getaddrinfo")
|
||||
@responses.activate
|
||||
def test_api_docs_cors_proxy_authenticated_user_accessing_protected_doc(
|
||||
mock_getaddrinfo,
|
||||
):
|
||||
def test_api_docs_cors_proxy_authenticated_user_accessing_protected_doc():
|
||||
"""
|
||||
Test the CORS proxy API for documents with an authenticated user accessing a protected
|
||||
document.
|
||||
"""
|
||||
document = factories.DocumentFactory(link_reach="authenticated")
|
||||
|
||||
# Mock DNS resolution to return a public IP address
|
||||
mock_getaddrinfo.return_value = [
|
||||
(socket.AF_INET, socket.SOCK_STREAM, 0, "", ("8.8.8.8", 0))
|
||||
]
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
@@ -138,22 +115,14 @@ def test_api_docs_cors_proxy_authenticated_user_accessing_protected_doc(
|
||||
assert response.streaming_content
|
||||
|
||||
|
||||
@unittest.mock.patch("core.api.viewsets.socket.getaddrinfo")
|
||||
@responses.activate
|
||||
def test_api_docs_cors_proxy_authenticated_not_accessing_restricted_doc(
|
||||
mock_getaddrinfo,
|
||||
):
|
||||
def test_api_docs_cors_proxy_authenticated_not_accessing_restricted_doc():
|
||||
"""
|
||||
Test the CORS proxy API for documents with an authenticated user not accessing a restricted
|
||||
document.
|
||||
"""
|
||||
document = factories.DocumentFactory(link_reach="restricted")
|
||||
|
||||
# Mock DNS resolution to return a public IP address
|
||||
mock_getaddrinfo.return_value = [
|
||||
(socket.AF_INET, socket.SOCK_STREAM, 0, "", ("8.8.8.8", 0))
|
||||
]
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
@@ -169,72 +138,18 @@ def test_api_docs_cors_proxy_authenticated_not_accessing_restricted_doc(
|
||||
}
|
||||
|
||||
|
||||
@unittest.mock.patch("core.api.viewsets.socket.getaddrinfo")
|
||||
@responses.activate
|
||||
def test_api_docs_cors_proxy_unsupported_media_type(mock_getaddrinfo):
|
||||
def test_api_docs_cors_proxy_unsupported_media_type():
|
||||
"""Test the CORS proxy API for documents with an unsupported media type."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
|
||||
# Mock DNS resolution to return a public IP address
|
||||
mock_getaddrinfo.return_value = [
|
||||
(socket.AF_INET, socket.SOCK_STREAM, 0, "", ("8.8.8.8", 0))
|
||||
]
|
||||
|
||||
client = APIClient()
|
||||
url_to_fetch = "https://external-url.com/assets/index.html"
|
||||
responses.get(url_to_fetch, body=b"", status=200, content_type="text/html")
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "Invalid URL used."}
|
||||
|
||||
|
||||
@unittest.mock.patch("core.api.viewsets.socket.getaddrinfo")
|
||||
@responses.activate
|
||||
def test_api_docs_cors_proxy_redirect(mock_getaddrinfo):
|
||||
"""Test the CORS proxy API for documents with a redirect."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
|
||||
# Mock DNS resolution to return a public IP address
|
||||
mock_getaddrinfo.return_value = [
|
||||
(socket.AF_INET, socket.SOCK_STREAM, 0, "", ("8.8.8.8", 0))
|
||||
]
|
||||
|
||||
client = APIClient()
|
||||
url_to_fetch = "https://external-url.com/assets/index.html"
|
||||
responses.get(
|
||||
url_to_fetch,
|
||||
body=b"",
|
||||
status=302,
|
||||
headers={"Location": "https://external-url.com/other/assets/index.html"},
|
||||
)
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "Invalid URL used."}
|
||||
|
||||
|
||||
@unittest.mock.patch("core.api.viewsets.socket.getaddrinfo")
|
||||
@responses.activate
|
||||
def test_api_docs_cors_proxy_url_not_returning_200(mock_getaddrinfo):
|
||||
"""Test the CORS proxy API for documents with a URL that does not return 200."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
|
||||
# Mock DNS resolution to return a public IP address
|
||||
mock_getaddrinfo.return_value = [
|
||||
(socket.AF_INET, socket.SOCK_STREAM, 0, "", ("8.8.8.8", 0))
|
||||
]
|
||||
|
||||
client = APIClient()
|
||||
url_to_fetch = "https://external-url.com/assets/index.html"
|
||||
responses.get(url_to_fetch, body=b"", status=404)
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "Invalid URL used."}
|
||||
assert response.status_code == 415
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -258,17 +173,11 @@ def test_api_docs_cors_proxy_invalid_url(url_to_fetch):
|
||||
assert response.json() == ["Enter a valid URL."]
|
||||
|
||||
|
||||
@unittest.mock.patch("core.api.viewsets.socket.getaddrinfo")
|
||||
@responses.activate
|
||||
def test_api_docs_cors_proxy_request_failed(mock_getaddrinfo):
|
||||
def test_api_docs_cors_proxy_request_failed():
|
||||
"""Test the CORS proxy API for documents with a request failed."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
|
||||
# Mock DNS resolution to return a public IP address
|
||||
mock_getaddrinfo.return_value = [
|
||||
(socket.AF_INET, socket.SOCK_STREAM, 0, "", ("8.8.8.8", 0))
|
||||
]
|
||||
|
||||
client = APIClient()
|
||||
url_to_fetch = "https://external-url.com/assets/index.html"
|
||||
responses.get(url_to_fetch, body=RequestException("Connection refused"))
|
||||
@@ -276,164 +185,6 @@ def test_api_docs_cors_proxy_request_failed(mock_getaddrinfo):
|
||||
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "Invalid URL used."}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url_to_fetch",
|
||||
[
|
||||
"http://localhost/image.png",
|
||||
"https://localhost/image.png",
|
||||
"http://127.0.0.1/image.png",
|
||||
"https://127.0.0.1/image.png",
|
||||
"http://0.0.0.0/image.png",
|
||||
"https://0.0.0.0/image.png",
|
||||
"http://[::1]/image.png",
|
||||
"https://[::1]/image.png",
|
||||
"http://[0:0:0:0:0:0:0:1]/image.png",
|
||||
"https://[0:0:0:0:0:0:0:1]/image.png",
|
||||
],
|
||||
)
|
||||
def test_api_docs_cors_proxy_blocks_localhost(url_to_fetch):
|
||||
"""Test that the CORS proxy API blocks localhost variations."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
|
||||
client = APIClient()
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "Invalid URL used."
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url_to_fetch",
|
||||
[
|
||||
"http://10.0.0.1/image.png",
|
||||
"https://10.0.0.1/image.png",
|
||||
"http://172.16.0.1/image.png",
|
||||
"https://172.16.0.1/image.png",
|
||||
"http://192.168.1.1/image.png",
|
||||
"https://192.168.1.1/image.png",
|
||||
"http://10.255.255.255/image.png",
|
||||
"https://10.255.255.255/image.png",
|
||||
"http://172.31.255.255/image.png",
|
||||
"https://172.31.255.255/image.png",
|
||||
"http://192.168.255.255/image.png",
|
||||
"https://192.168.255.255/image.png",
|
||||
],
|
||||
)
|
||||
def test_api_docs_cors_proxy_blocks_private_ips(url_to_fetch):
|
||||
"""Test that the CORS proxy API blocks private IP addresses."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
|
||||
client = APIClient()
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "Invalid URL used."
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url_to_fetch",
|
||||
[
|
||||
"http://169.254.1.1/image.png",
|
||||
"https://169.254.1.1/image.png",
|
||||
"http://169.254.255.255/image.png",
|
||||
"https://169.254.255.255/image.png",
|
||||
],
|
||||
)
|
||||
def test_api_docs_cors_proxy_blocks_link_local(url_to_fetch):
|
||||
"""Test that the CORS proxy API blocks link-local addresses."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
|
||||
client = APIClient()
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "Invalid URL used."
|
||||
|
||||
|
||||
@unittest.mock.patch("core.api.viewsets.socket.getaddrinfo")
|
||||
@responses.activate
|
||||
def test_api_docs_cors_proxy_blocks_dns_rebinding_to_private_ip(mock_getaddrinfo):
|
||||
"""Test that the CORS proxy API blocks DNS rebinding attacks to private IPs."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
|
||||
# Mock DNS resolution to return a private IP address
|
||||
mock_getaddrinfo.return_value = [
|
||||
(socket.AF_INET, socket.SOCK_STREAM, 0, "", ("192.168.1.1", 0))
|
||||
]
|
||||
|
||||
client = APIClient()
|
||||
url_to_fetch = "https://malicious-domain.com/image.png"
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "Invalid URL used."
|
||||
mock_getaddrinfo.assert_called_once()
|
||||
|
||||
|
||||
@unittest.mock.patch("core.api.viewsets.socket.getaddrinfo")
|
||||
@responses.activate
|
||||
def test_api_docs_cors_proxy_blocks_dns_rebinding_to_localhost(mock_getaddrinfo):
|
||||
"""Test that the CORS proxy API blocks DNS rebinding attacks to localhost."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
|
||||
# Mock DNS resolution to return localhost
|
||||
mock_getaddrinfo.return_value = [
|
||||
(socket.AF_INET, socket.SOCK_STREAM, 0, "", ("127.0.0.1", 0))
|
||||
]
|
||||
|
||||
client = APIClient()
|
||||
url_to_fetch = "https://malicious-domain.com/image.png"
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "Invalid URL used."
|
||||
mock_getaddrinfo.assert_called_once()
|
||||
|
||||
|
||||
@unittest.mock.patch("core.api.viewsets.socket.getaddrinfo")
|
||||
def test_api_docs_cors_proxy_handles_dns_resolution_failure(mock_getaddrinfo):
|
||||
"""Test that the CORS proxy API handles DNS resolution failures gracefully."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
|
||||
# Mock DNS resolution to fail
|
||||
mock_getaddrinfo.side_effect = socket.gaierror("Name or service not known")
|
||||
|
||||
client = APIClient()
|
||||
url_to_fetch = "https://nonexistent-domain-12345.com/image.png"
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "Invalid URL used."
|
||||
mock_getaddrinfo.assert_called_once()
|
||||
|
||||
|
||||
@unittest.mock.patch("core.api.viewsets.socket.getaddrinfo")
|
||||
def test_api_docs_cors_proxy_blocks_multiple_resolved_ips_if_any_private(
|
||||
mock_getaddrinfo,
|
||||
):
|
||||
"""Test that the CORS proxy API blocks if any resolved IP is private."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
|
||||
# Mock DNS resolution to return both public and private IPs
|
||||
mock_getaddrinfo.return_value = [
|
||||
(socket.AF_INET, socket.SOCK_STREAM, 0, "", ("8.8.8.8", 0)),
|
||||
(socket.AF_INET, socket.SOCK_STREAM, 0, "", ("192.168.1.1", 0)),
|
||||
]
|
||||
|
||||
client = APIClient()
|
||||
url_to_fetch = "https://example.com/image.png"
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "Invalid URL used."
|
||||
mock_getaddrinfo.assert_called_once()
|
||||
assert response.json() == {
|
||||
"error": "Failed to fetch resource from https://external-url.com/assets/index.html"
|
||||
}
|
||||
|
||||
@@ -311,7 +311,7 @@ def test_api_users_list_query_short_queries():
|
||||
"""
|
||||
Queries shorter than 5 characters should return an empty result set.
|
||||
"""
|
||||
user = factories.UserFactory(email="paul@example.com", full_name="Paul")
|
||||
user = factories.UserFactory(email="paul@example.com")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
|
||||
@@ -1393,7 +1393,7 @@ def test_models_documents_restore_complex(django_assert_num_queries):
|
||||
assert child2.ancestors_deleted_at == document.deleted_at
|
||||
|
||||
# Restore the item
|
||||
with django_assert_num_queries(14):
|
||||
with django_assert_num_queries(13):
|
||||
document.restore()
|
||||
document.refresh_from_db()
|
||||
child1.refresh_from_db()
|
||||
|
||||
@@ -453,7 +453,7 @@ class Base(Configuration):
|
||||
"REDOC_DIST": "SIDECAR",
|
||||
}
|
||||
|
||||
TRASHBIN_CUTOFF_DAYS = values.IntegerValue(
|
||||
TRASHBIN_CUTOFF_DAYS = values.Value(
|
||||
30, environ_name="TRASHBIN_CUTOFF_DAYS", environ_prefix=None
|
||||
)
|
||||
|
||||
@@ -699,16 +699,6 @@ class Base(Configuration):
|
||||
"day": 200,
|
||||
}
|
||||
|
||||
LANGFUSE_SECRET_KEY = SecretFileValue(
|
||||
None, environ_name="LANGFUSE_SECRET_KEY", environ_prefix=None
|
||||
)
|
||||
LANGFUSE_PUBLIC_KEY = values.Value(
|
||||
None, environ_name="LANGFUSE_PUBLIC_KEY", environ_prefix=None
|
||||
)
|
||||
LANGFUSE_BASE_URL = values.Value(
|
||||
None, environ_name="LANGFUSE_BASE_URL", environ_prefix=None
|
||||
)
|
||||
|
||||
# Y provider microservice
|
||||
Y_PROVIDER_API_KEY = SecretFileValue(
|
||||
environ_name="Y_PROVIDER_API_KEY",
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 13:17\n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Breton\n"
|
||||
"Language: br_FR\n"
|
||||
@@ -79,15 +79,11 @@ msgstr "Doare korf"
|
||||
msgid "Format"
|
||||
msgstr "Stumm"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#, 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"
|
||||
@@ -243,8 +239,8 @@ msgstr "implijer"
|
||||
msgid "users"
|
||||
msgstr "implijerien"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
msgid "title"
|
||||
msgstr "titl"
|
||||
|
||||
@@ -260,188 +256,188 @@ msgstr "Restr"
|
||||
msgid "Documents"
|
||||
msgstr "Restroù"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
msgid "Untitled Document"
|
||||
msgstr "Restr hep titl"
|
||||
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} en deus rannet ur restr ganeoc'h!"
|
||||
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#, 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:872 core/models.py:872
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#, 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:973 core/models.py:973
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Roud liamm ar restr/an implijer"
|
||||
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Roudoù liamm ar restr/an implijer"
|
||||
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
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:1003 core/models.py:1003
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
msgid "Document favorite"
|
||||
msgstr "Restr muiañ-karet"
|
||||
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
msgid "Document favorites"
|
||||
msgstr "Restroù muiañ-karet"
|
||||
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
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:1032 core/models.py:1032
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
msgid "Document/user relation"
|
||||
msgstr "Liamm restr/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
msgid "Document/user relations"
|
||||
msgstr "Liammoù restr/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
msgid "This user is already in this document."
|
||||
msgstr "An implijer-mañ a zo dija er restr-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
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:1202 core/models.py:1202
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "Document ask for access"
|
||||
msgstr "Goulenn tizhout ar restr"
|
||||
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Goulennoù tizhout ar restr"
|
||||
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
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:1266 core/models.py:1266
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, 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:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, 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:1276 core/models.py:1276
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, 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:1318 core/models.py:1318
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
msgid "description"
|
||||
msgstr "deskrivadur"
|
||||
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
msgid "code"
|
||||
msgstr "kod"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "public"
|
||||
msgstr "publik"
|
||||
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
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:1447 core/models.py:1447
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
msgid "Template"
|
||||
msgstr "Patrom"
|
||||
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
msgid "Templates"
|
||||
msgstr "Patromoù"
|
||||
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
msgid "Template/user relation"
|
||||
msgstr "Liamm patrom/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
msgid "Template/user relations"
|
||||
msgstr "Liammoù patrom/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
msgid "This user is already in this template."
|
||||
msgstr "An implijer-mañ a zo dija er patrom-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
msgid "email address"
|
||||
msgstr "postel"
|
||||
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
msgid "Document invitation"
|
||||
msgstr "Pedadenn d'ur restr"
|
||||
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
msgid "Document invitations"
|
||||
msgstr "Pedadennoù d'ur restr"
|
||||
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
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: 2026-01-08 15:38+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 13:17\n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -79,15 +79,11 @@ msgstr "Typ"
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#, 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"
|
||||
@@ -243,8 +239,8 @@ msgstr "Benutzer"
|
||||
msgid "users"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
msgid "title"
|
||||
msgstr "Titel"
|
||||
|
||||
@@ -260,188 +256,188 @@ msgstr "Dokument"
|
||||
msgid "Documents"
|
||||
msgstr "Dokumente"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
msgid "Untitled Document"
|
||||
msgstr "Unbenanntes Dokument"
|
||||
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
|
||||
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#, 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:872 core/models.py:872
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#, 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:973 core/models.py:973
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
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:1003 core/models.py:1003
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
msgid "Document favorite"
|
||||
msgstr "Dokumentenfavorit"
|
||||
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
msgid "Document favorites"
|
||||
msgstr "Dokumentfavoriten"
|
||||
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
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:1032 core/models.py:1032
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
msgid "Document/user relation"
|
||||
msgstr "Dokument/Benutzerbeziehung"
|
||||
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
msgid "Document/user relations"
|
||||
msgstr "Dokument/Benutzerbeziehungen"
|
||||
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
|
||||
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
|
||||
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
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:1202 core/models.py:1202
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#: 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:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
msgid "description"
|
||||
msgstr "Beschreibung"
|
||||
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
msgid "code"
|
||||
msgstr "Code"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
msgid "css"
|
||||
msgstr "CSS"
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "public"
|
||||
msgstr "öffentlich"
|
||||
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
|
||||
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
msgid "Template"
|
||||
msgstr "Vorlage"
|
||||
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
msgid "Templates"
|
||||
msgstr "Vorlagen"
|
||||
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
msgid "Template/user relation"
|
||||
msgstr "Vorlage/Benutzer-Beziehung"
|
||||
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
msgid "Template/user relations"
|
||||
msgstr "Vorlage/Benutzerbeziehungen"
|
||||
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
|
||||
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Dieses Team ist bereits in diesem Template."
|
||||
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
msgid "email address"
|
||||
msgstr "E-Mail-Adresse"
|
||||
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
msgid "Document invitation"
|
||||
msgstr "Einladung zum Dokument"
|
||||
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
msgid "Document invitations"
|
||||
msgstr "Dokumenteinladungen"
|
||||
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
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: 2026-01-08 15:38+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 13:17\n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_US\n"
|
||||
@@ -79,15 +79,11 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#, 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"
|
||||
@@ -243,8 +239,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -260,188 +256,188 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#: 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:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
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: 2026-01-08 15:38+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 13:17\n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Language: es_ES\n"
|
||||
@@ -79,15 +79,11 @@ msgstr "Tipo de Cuerpo"
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#, 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"
|
||||
@@ -243,8 +239,8 @@ msgstr "usuario"
|
||||
msgid "users"
|
||||
msgstr "usuarios"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
msgid "title"
|
||||
msgstr "título"
|
||||
|
||||
@@ -260,188 +256,188 @@ msgstr "Documento"
|
||||
msgid "Documents"
|
||||
msgstr "Documentos"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
msgid "Untitled Document"
|
||||
msgstr "Documento sin título"
|
||||
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "¡{name} ha compartido un documento contigo!"
|
||||
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#, 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:872 core/models.py:872
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} ha compartido un documento contigo: {title}"
|
||||
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Traza del enlace de documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Trazas del enlace de documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
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:1003 core/models.py:1003
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
msgid "Document favorite"
|
||||
msgstr "Documento favorito"
|
||||
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
msgid "Document favorites"
|
||||
msgstr "Documentos favoritos"
|
||||
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
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:1032 core/models.py:1032
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
msgid "Document/user relation"
|
||||
msgstr "Relación documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
msgid "Document/user relations"
|
||||
msgstr "Relaciones documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Este usuario ya forma parte del documento."
|
||||
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Este equipo ya forma parte del documento."
|
||||
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
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:1202 core/models.py:1202
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "Document ask for access"
|
||||
msgstr "Solicitud de acceso"
|
||||
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Solicitud de accesos"
|
||||
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
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:1266 core/models.py:1266
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "¡{name} desea acceder a un documento!"
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} desea acceso al siguiente documento:"
|
||||
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, 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:1318 core/models.py:1318
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
msgid "description"
|
||||
msgstr "descripción"
|
||||
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
msgid "code"
|
||||
msgstr "código"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "public"
|
||||
msgstr "público"
|
||||
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
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:1447 core/models.py:1447
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
msgid "Template"
|
||||
msgstr "Plantilla"
|
||||
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
msgid "Templates"
|
||||
msgstr "Plantillas"
|
||||
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
msgid "Template/user relation"
|
||||
msgstr "Relación plantilla/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
msgid "Template/user relations"
|
||||
msgstr "Relaciones plantilla/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Este usuario ya forma parte de la plantilla."
|
||||
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Este equipo ya se encuentra en esta plantilla."
|
||||
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
msgid "email address"
|
||||
msgstr "dirección de correo electrónico"
|
||||
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
msgid "Document invitation"
|
||||
msgstr "Invitación al documento"
|
||||
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
msgid "Document invitations"
|
||||
msgstr "Invitaciones a documentos"
|
||||
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
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: 2026-01-08 15:38+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 13:17\n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr_FR\n"
|
||||
@@ -79,15 +79,11 @@ msgstr "Type de corps"
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#, 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"
|
||||
@@ -243,8 +239,8 @@ msgstr "utilisateur"
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
msgid "title"
|
||||
msgstr "titre"
|
||||
|
||||
@@ -260,188 +256,188 @@ msgstr "Document"
|
||||
msgid "Documents"
|
||||
msgstr "Documents"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
msgid "Untitled Document"
|
||||
msgstr "Document sans titre"
|
||||
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} a partagé un document avec vous!"
|
||||
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#, 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:872 core/models.py:872
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#, 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:973 core/models.py:973
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Trace du lien document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Traces du lien document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
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:1003 core/models.py:1003
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
msgid "Document favorite"
|
||||
msgstr "Document favori"
|
||||
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
msgid "Document favorites"
|
||||
msgstr "Documents favoris"
|
||||
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
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:1032 core/models.py:1032
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
msgid "Document/user relation"
|
||||
msgstr "Relation document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
msgid "Document/user relations"
|
||||
msgstr "Relations document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Cet utilisateur est déjà dans ce document."
|
||||
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Cette équipe est déjà dans ce document."
|
||||
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
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:1202 core/models.py:1202
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "Document ask for access"
|
||||
msgstr "Demande d'accès au document"
|
||||
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Demande d'accès au document"
|
||||
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
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:1266 core/models.py:1266
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} souhaiterait accéder au document suivant !"
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, 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:1276 core/models.py:1276
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, 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:1318 core/models.py:1318
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
msgid "Thread"
|
||||
msgstr "Conversation"
|
||||
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
msgid "Threads"
|
||||
msgstr "Conversations"
|
||||
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
msgid "Anonymous"
|
||||
msgstr "Anonyme"
|
||||
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
msgid "Comment"
|
||||
msgstr "Commentaire"
|
||||
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
msgid "Comments"
|
||||
msgstr "Commentaires"
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
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:1423 core/models.py:1423
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "Reaction"
|
||||
msgstr "Réaction"
|
||||
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
msgid "Reactions"
|
||||
msgstr "Réactions"
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
msgid "description"
|
||||
msgstr "description"
|
||||
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
msgid "css"
|
||||
msgstr "CSS"
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "public"
|
||||
msgstr "public"
|
||||
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
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:1447 core/models.py:1447
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
msgid "Template"
|
||||
msgstr "Modèle"
|
||||
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
msgid "Templates"
|
||||
msgstr "Modèles"
|
||||
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
msgid "Template/user relation"
|
||||
msgstr "Relation modèle/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
msgid "Template/user relations"
|
||||
msgstr "Relations modèle/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Cet utilisateur est déjà dans ce modèle."
|
||||
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Cette équipe est déjà modèle."
|
||||
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
msgid "email address"
|
||||
msgstr "adresse e-mail"
|
||||
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
msgid "Document invitation"
|
||||
msgstr "Invitation à un document"
|
||||
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
msgid "Document invitations"
|
||||
msgstr "Invitations à un document"
|
||||
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
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: 2026-01-08 15:38+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 13:17\n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Italian\n"
|
||||
"Language: it_IT\n"
|
||||
@@ -79,15 +79,11 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#, 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"
|
||||
@@ -243,8 +239,8 @@ msgstr "utente"
|
||||
msgid "users"
|
||||
msgstr "utenti"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
msgid "title"
|
||||
msgstr "titolo"
|
||||
|
||||
@@ -260,188 +256,188 @@ msgstr "Documento"
|
||||
msgid "Documents"
|
||||
msgstr "Documenti"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
msgid "Untitled Document"
|
||||
msgstr "Documento senza titolo"
|
||||
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} ha condiviso un documento con te!"
|
||||
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#, 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:872 core/models.py:872
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#, 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:973 core/models.py:973
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
msgid "Document favorite"
|
||||
msgstr "Documento preferito"
|
||||
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
msgid "Document favorites"
|
||||
msgstr "Documenti preferiti"
|
||||
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Questo utente è già presente in questo documento."
|
||||
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Questo team è già presente in questo documento."
|
||||
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#: 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:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
msgid "description"
|
||||
msgstr "descrizione"
|
||||
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "public"
|
||||
msgstr "pubblico"
|
||||
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Indica se questo modello è pubblico per chiunque."
|
||||
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
msgid "Template"
|
||||
msgstr "Modello"
|
||||
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
msgid "Templates"
|
||||
msgstr "Modelli"
|
||||
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Questo utente è già in questo modello."
|
||||
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Questo team è già in questo modello."
|
||||
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
msgid "email address"
|
||||
msgstr "indirizzo e-mail"
|
||||
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
msgid "Document invitation"
|
||||
msgstr "Invito al documento"
|
||||
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
msgid "Document invitations"
|
||||
msgstr "Inviti al documento"
|
||||
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
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: 2026-01-08 15:38+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 13:17\n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Language: nl_NL\n"
|
||||
@@ -79,15 +79,11 @@ msgstr "Text type"
|
||||
msgid "Format"
|
||||
msgstr "Formaat"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#, 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"
|
||||
@@ -243,8 +239,8 @@ msgstr "gebruiker"
|
||||
msgid "users"
|
||||
msgstr "gebruikers"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
msgid "title"
|
||||
msgstr "titel"
|
||||
|
||||
@@ -260,188 +256,188 @@ msgstr "Document"
|
||||
msgid "Documents"
|
||||
msgstr "Documenten"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
msgid "Untitled Document"
|
||||
msgstr "Naamloos Document"
|
||||
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} heeft een document met u gedeeld!"
|
||||
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#, 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:872 core/models.py:872
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#, 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:973 core/models.py:973
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Document/gebruiker link"
|
||||
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Document/gebruiker link"
|
||||
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
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:1003 core/models.py:1003
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
msgid "Document favorite"
|
||||
msgstr "Document favoriet"
|
||||
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
msgid "Document favorites"
|
||||
msgstr "Document favorieten"
|
||||
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
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:1032 core/models.py:1032
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
msgid "Document/user relation"
|
||||
msgstr "Document/gebruiker relatie"
|
||||
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
msgid "Document/user relations"
|
||||
msgstr "Document/gebruiker relaties"
|
||||
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
msgid "This user is already in this document."
|
||||
msgstr "De gebruiker bestaat al in dit document."
|
||||
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Dit team bestaat al in dit document."
|
||||
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
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:1202 core/models.py:1202
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "Document ask for access"
|
||||
msgstr "Document verzoekt om toegang"
|
||||
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Document verzoekt om toegangen"
|
||||
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
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:1266 core/models.py:1266
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} verzoekt toegang tot een document!"
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, 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:1276 core/models.py:1276
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, 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:1318 core/models.py:1318
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
msgid "Thread"
|
||||
msgstr "Kanaal"
|
||||
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
msgid "Threads"
|
||||
msgstr "Kanalen"
|
||||
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
msgid "Anonymous"
|
||||
msgstr "Anoniem"
|
||||
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
msgid "Comment"
|
||||
msgstr "Reactie"
|
||||
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
msgid "Comments"
|
||||
msgstr "Reacties"
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr "Deze emoji is al op deze opmerking gereageerd."
|
||||
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "Reaction"
|
||||
msgstr "Reactie"
|
||||
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
msgid "Reactions"
|
||||
msgstr "Reacties"
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
msgid "description"
|
||||
msgstr "omschrijving"
|
||||
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "public"
|
||||
msgstr "publiek"
|
||||
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
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:1447 core/models.py:1447
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
msgid "Template"
|
||||
msgstr "Sjabloon"
|
||||
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
msgid "Templates"
|
||||
msgstr "Sjabloon"
|
||||
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
msgid "Template/user relation"
|
||||
msgstr "Sjabloon/gebruiker relatie"
|
||||
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
msgid "Template/user relations"
|
||||
msgstr "Sjabloon/gebruiker relaties"
|
||||
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
msgid "This user is already in this template."
|
||||
msgstr "De gebruiker bestaat al in dit sjabloon."
|
||||
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Het team bestaat al in dit sjabloon."
|
||||
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
msgid "email address"
|
||||
msgstr "e-mailadres"
|
||||
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
msgid "Document invitation"
|
||||
msgstr "Document uitnodiging"
|
||||
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
msgid "Document invitations"
|
||||
msgstr "Document uitnodigingen"
|
||||
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
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: 2026-01-08 15:38+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 13:17\n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Portuguese\n"
|
||||
"Language: pt_PT\n"
|
||||
@@ -79,15 +79,11 @@ msgstr "Tipo de corpo"
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#, 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"
|
||||
@@ -243,8 +239,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -260,188 +256,188 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#: 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:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
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: 2026-01-08 15:38+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 13:17\n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Russian\n"
|
||||
"Language: ru_RU\n"
|
||||
@@ -79,15 +79,11 @@ msgstr "Тип сообщения"
|
||||
msgid "Format"
|
||||
msgstr "Формат"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#, 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"
|
||||
@@ -243,8 +239,8 @@ msgstr "пользователь"
|
||||
msgid "users"
|
||||
msgstr "пользователи"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
msgid "title"
|
||||
msgstr "заголовок"
|
||||
|
||||
@@ -260,188 +256,188 @@ msgstr "Документ"
|
||||
msgid "Documents"
|
||||
msgstr "Документы"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
msgid "Untitled Document"
|
||||
msgstr "Безымянный документ"
|
||||
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} делится с вами документом!"
|
||||
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
|
||||
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} делится с вами документом: {title}"
|
||||
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Трассировка связи документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Трассировка связей документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
|
||||
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
msgid "Document favorite"
|
||||
msgstr "Избранный документ"
|
||||
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
msgid "Document favorites"
|
||||
msgstr "Избранные документы"
|
||||
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Этот документ уже помечен как избранный для этого пользователя."
|
||||
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
msgid "Document/user relation"
|
||||
msgstr "Отношение документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
msgid "Document/user relations"
|
||||
msgstr "Отношения документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Этот пользователь уже имеет доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Эта команда уже имеет доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
|
||||
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "Document ask for access"
|
||||
msgstr "Документ запрашивает доступ"
|
||||
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Документ запрашивает доступы"
|
||||
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "Этот пользователь уже запросил доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} хочет получить доступ к документу!"
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} хочет получить доступ к следующему документу:"
|
||||
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name} запрашивает доступ к документу: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
msgid "Thread"
|
||||
msgstr "Обсуждение"
|
||||
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
msgid "Threads"
|
||||
msgstr "Обсуждения"
|
||||
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
msgid "Anonymous"
|
||||
msgstr "Аноним"
|
||||
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
msgid "Comment"
|
||||
msgstr "Комментарий"
|
||||
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
msgid "Comments"
|
||||
msgstr "Комментарии"
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr "Этот эмодзи уже использован в этом комментарии."
|
||||
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "Reaction"
|
||||
msgstr "Реакция"
|
||||
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
msgid "Reactions"
|
||||
msgstr "Реакции"
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
msgid "description"
|
||||
msgstr "описание"
|
||||
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
msgid "code"
|
||||
msgstr "код"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "public"
|
||||
msgstr "доступно всем"
|
||||
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Этот шаблон доступен всем пользователям."
|
||||
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
msgid "Template"
|
||||
msgstr "Шаблон"
|
||||
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
msgid "Templates"
|
||||
msgstr "Шаблоны"
|
||||
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
msgid "Template/user relation"
|
||||
msgstr "Отношение шаблон/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
msgid "Template/user relations"
|
||||
msgstr "Отношения шаблон/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Этот пользователь уже указан в этом шаблоне."
|
||||
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Эта команда уже указана в этом шаблоне."
|
||||
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
msgid "email address"
|
||||
msgstr "адрес электронной почты"
|
||||
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
msgid "Document invitation"
|
||||
msgstr "Приглашение для документа"
|
||||
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
msgid "Document invitations"
|
||||
msgstr "Приглашения для документов"
|
||||
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
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: 2026-01-08 15:38+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 13:17\n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Slovenian\n"
|
||||
"Language: sl_SI\n"
|
||||
@@ -79,15 +79,11 @@ msgstr "Vrsta telesa"
|
||||
msgid "Format"
|
||||
msgstr "Oblika"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#, 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"
|
||||
@@ -243,8 +239,8 @@ msgstr "uporabnik"
|
||||
msgid "users"
|
||||
msgstr "uporabniki"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
msgid "title"
|
||||
msgstr "naslov"
|
||||
|
||||
@@ -260,188 +256,188 @@ msgstr "Dokument"
|
||||
msgid "Documents"
|
||||
msgstr "Dokumenti"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
msgid "Untitled Document"
|
||||
msgstr "Dokument brez naslova"
|
||||
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} je delil dokument z vami!"
|
||||
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#, 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:872 core/models.py:872
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} je delil dokument z vami: {title}"
|
||||
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Dokument/sled povezave uporabnika"
|
||||
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Sledi povezav dokumenta/uporabnika"
|
||||
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
|
||||
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
msgid "Document favorite"
|
||||
msgstr "Priljubljeni dokument"
|
||||
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
msgid "Document favorites"
|
||||
msgstr "Priljubljeni dokumenti"
|
||||
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
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:1032 core/models.py:1032
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
msgid "Document/user relation"
|
||||
msgstr "Odnos dokument/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
msgid "Document/user relations"
|
||||
msgstr "Odnosi dokument/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Ta uporabnik je že v tem dokumentu."
|
||||
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ta ekipa je že v tem dokumentu."
|
||||
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
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:1202 core/models.py:1202
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#: 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:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
msgid "description"
|
||||
msgstr "opis"
|
||||
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
msgid "code"
|
||||
msgstr "koda"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "public"
|
||||
msgstr "javno"
|
||||
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Ali je ta predloga javna za uporabo."
|
||||
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
msgid "Template"
|
||||
msgstr "Predloga"
|
||||
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
msgid "Templates"
|
||||
msgstr "Predloge"
|
||||
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
msgid "Template/user relation"
|
||||
msgstr "Odnos predloga/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
msgid "Template/user relations"
|
||||
msgstr "Odnosi med predlogo in uporabnikom"
|
||||
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Ta uporabnik je že v tej predlogi."
|
||||
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Ta ekipa je že v tej predlogi."
|
||||
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
msgid "email address"
|
||||
msgstr "elektronski naslov"
|
||||
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
msgid "Document invitation"
|
||||
msgstr "Vabilo na dokument"
|
||||
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
msgid "Document invitations"
|
||||
msgstr "Vabila na dokument"
|
||||
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
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: 2026-01-08 15:38+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 13:17\n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Swedish\n"
|
||||
"Language: sv_SE\n"
|
||||
@@ -79,15 +79,11 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#, 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"
|
||||
@@ -243,8 +239,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -260,188 +256,188 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#: 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:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
msgid "email address"
|
||||
msgstr "e-postadress"
|
||||
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
msgid "Document invitation"
|
||||
msgstr "Bjud in dokument"
|
||||
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
msgid "Document invitations"
|
||||
msgstr "Inbjudningar dokument"
|
||||
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
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: 2026-01-08 15:38+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 13:17\n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Turkish\n"
|
||||
"Language: tr_TR\n"
|
||||
@@ -79,15 +79,11 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#, 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"
|
||||
@@ -243,8 +239,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -260,188 +256,188 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#: 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:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
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: 2026-01-08 15:38+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 13:17\n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Ukrainian\n"
|
||||
"Language: uk_UA\n"
|
||||
@@ -79,15 +79,11 @@ msgstr "Тип вмісту"
|
||||
msgid "Format"
|
||||
msgstr "Формат"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#, 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"
|
||||
@@ -243,8 +239,8 @@ msgstr "користувач"
|
||||
msgid "users"
|
||||
msgstr "користувачі"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
msgid "title"
|
||||
msgstr "заголовок"
|
||||
|
||||
@@ -260,188 +256,188 @@ msgstr "Документ"
|
||||
msgid "Documents"
|
||||
msgstr "Документи"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
msgid "Untitled Document"
|
||||
msgstr "Документ без назви"
|
||||
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} ділиться з вами документом!"
|
||||
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
|
||||
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} ділиться з вами документом: {title}"
|
||||
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Трасування посилання Документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Трасування посилань Документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
|
||||
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
msgid "Document favorite"
|
||||
msgstr "Обраний документ"
|
||||
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
msgid "Document favorites"
|
||||
msgstr "Обрані документи"
|
||||
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Цей документ вже вказаний як обраний для одного користувача."
|
||||
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
msgid "Document/user relation"
|
||||
msgstr "Відносини документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
msgid "Document/user relations"
|
||||
msgstr "Відносини документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Цей користувач вже має доступ до цього документу."
|
||||
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ця команда вже має доступ до цього документа."
|
||||
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Вкажіть користувача або команду, а не обох."
|
||||
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "Document ask for access"
|
||||
msgstr "Запит доступу до документа"
|
||||
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Запит доступу для документа"
|
||||
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "Цей користувач вже попросив доступ до цього документа."
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} хоче отримати доступ до документа!"
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} бажає отримати доступ до наступного документа:"
|
||||
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name} запитує доступ до документа: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
msgid "Thread"
|
||||
msgstr "Обговорення"
|
||||
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
msgid "Threads"
|
||||
msgstr "Обговорення"
|
||||
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
msgid "Anonymous"
|
||||
msgstr "Анонім"
|
||||
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
msgid "Comment"
|
||||
msgstr "Коментар"
|
||||
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
msgid "Comments"
|
||||
msgstr "Коментарі"
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr "Цим емодзі вже відреагували на цей коментар."
|
||||
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "Reaction"
|
||||
msgstr "Реакція"
|
||||
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
msgid "Reactions"
|
||||
msgstr "Реакції"
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
msgid "description"
|
||||
msgstr "опис"
|
||||
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
msgid "code"
|
||||
msgstr "код"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "public"
|
||||
msgstr "публічне"
|
||||
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Чи є цей шаблон публічним для будь-кого користувача."
|
||||
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
msgid "Template"
|
||||
msgstr "Шаблон"
|
||||
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
msgid "Templates"
|
||||
msgstr "Шаблони"
|
||||
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
msgid "Template/user relation"
|
||||
msgstr "Відношення шаблон/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
msgid "Template/user relations"
|
||||
msgstr "Відношення шаблон/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Цей користувач вже має доступ до цього шаблону."
|
||||
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Ця команда вже має доступ до цього шаблону."
|
||||
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
msgid "email address"
|
||||
msgstr "електронна адреса"
|
||||
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
msgid "Document invitation"
|
||||
msgstr "Запрошення до редагування документа"
|
||||
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
msgid "Document invitations"
|
||||
msgstr "Запрошення до редагування документів"
|
||||
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
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: 2026-01-08 15:38+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 13:17\n"
|
||||
"POT-Creation-Date: 2025-11-20 14:08+0000\n"
|
||||
"PO-Revision-Date: 2025-12-09 11:12\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"Language: zh_CN\n"
|
||||
@@ -79,15 +79,11 @@ msgstr "正文类型"
|
||||
msgid "Format"
|
||||
msgstr "格式"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
|
||||
#: build/lib/core/api/viewsets.py:1004 core/api/viewsets.py:1004
|
||||
#, 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"
|
||||
@@ -243,8 +239,8 @@ msgstr "用户"
|
||||
msgid "users"
|
||||
msgstr "个用户"
|
||||
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
|
||||
#: core/models.py:361 core/models.py:1434
|
||||
#: build/lib/core/models.py:361 build/lib/core/models.py:1430
|
||||
#: core/models.py:361 core/models.py:1430
|
||||
msgid "title"
|
||||
msgstr "标题"
|
||||
|
||||
@@ -260,188 +256,188 @@ msgstr "文档"
|
||||
msgid "Documents"
|
||||
msgstr "个文档"
|
||||
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
|
||||
#: core/models.py:827
|
||||
#: build/lib/core/models.py:424 build/lib/core/models.py:824 core/models.py:424
|
||||
#: core/models.py:824
|
||||
msgid "Untitled Document"
|
||||
msgstr "未命名文档"
|
||||
|
||||
#: build/lib/core/models.py:862 core/models.py:862
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} 与您共享了一个文档!"
|
||||
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#: build/lib/core/models.py:863 core/models.py:863
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
|
||||
|
||||
#: build/lib/core/models.py:872 core/models.py:872
|
||||
#: build/lib/core/models.py:869 core/models.py:869
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} 与您共享了一个文档:{title}"
|
||||
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
#: build/lib/core/models.py:969 core/models.py:969
|
||||
msgid "Document/user link trace"
|
||||
msgstr "文档/用户链接跟踪"
|
||||
|
||||
#: build/lib/core/models.py:974 core/models.py:974
|
||||
#: build/lib/core/models.py:970 core/models.py:970
|
||||
msgid "Document/user link traces"
|
||||
msgstr "个文档/用户链接跟踪"
|
||||
|
||||
#: build/lib/core/models.py:980 core/models.py:980
|
||||
#: build/lib/core/models.py:976 core/models.py:976
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "此文档/用户的链接跟踪已存在。"
|
||||
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
#: build/lib/core/models.py:999 core/models.py:999
|
||||
msgid "Document favorite"
|
||||
msgstr "文档收藏"
|
||||
|
||||
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
msgid "Document favorites"
|
||||
msgstr "文档收藏夹"
|
||||
|
||||
#: build/lib/core/models.py:1010 core/models.py:1010
|
||||
#: build/lib/core/models.py:1006 core/models.py:1006
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "该文档已被同一用户的收藏关系实例关联。"
|
||||
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
#: build/lib/core/models.py:1028 core/models.py:1028
|
||||
msgid "Document/user relation"
|
||||
msgstr "文档/用户关系"
|
||||
|
||||
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||
#: build/lib/core/models.py:1029 core/models.py:1029
|
||||
msgid "Document/user relations"
|
||||
msgstr "文档/用户关系集"
|
||||
|
||||
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||
#: build/lib/core/models.py:1035 core/models.py:1035
|
||||
msgid "This user is already in this document."
|
||||
msgstr "该用户已在此文档中。"
|
||||
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1041 core/models.py:1041
|
||||
msgid "This team is already in this document."
|
||||
msgstr "该团队已在此文档中。"
|
||||
|
||||
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
|
||||
#: core/models.py:1051 core/models.py:1520
|
||||
#: build/lib/core/models.py:1047 build/lib/core/models.py:1516
|
||||
#: core/models.py:1047 core/models.py:1516
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "必须设置用户或团队之一,不能同时设置两者。"
|
||||
|
||||
#: build/lib/core/models.py:1202 core/models.py:1202
|
||||
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||
msgid "Document ask for access"
|
||||
msgstr "文档需要访问权限"
|
||||
|
||||
#: build/lib/core/models.py:1203 core/models.py:1203
|
||||
#: build/lib/core/models.py:1199 core/models.py:1199
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "文档需要访问权限"
|
||||
|
||||
#: build/lib/core/models.py:1209 core/models.py:1209
|
||||
#: build/lib/core/models.py:1205 core/models.py:1205
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "用户已申请该文档的访问权限。"
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} 申请访问文档!"
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} 申请访问以下文档:"
|
||||
|
||||
#: build/lib/core/models.py:1276 core/models.py:1276
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name}申请文档:{title}的访问权限"
|
||||
|
||||
#: build/lib/core/models.py:1318 core/models.py:1318
|
||||
#: build/lib/core/models.py:1314 core/models.py:1314
|
||||
msgid "Thread"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1319 core/models.py:1319
|
||||
#: build/lib/core/models.py:1315 core/models.py:1315
|
||||
msgid "Threads"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
|
||||
#: core/models.py:1322 core/models.py:1374
|
||||
#: build/lib/core/models.py:1318 build/lib/core/models.py:1370
|
||||
#: core/models.py:1318 core/models.py:1370
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1369 core/models.py:1369
|
||||
#: build/lib/core/models.py:1365 core/models.py:1365
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1370 core/models.py:1370
|
||||
#: build/lib/core/models.py:1366 core/models.py:1366
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
#: build/lib/core/models.py:1415 core/models.py:1415
|
||||
msgid "This emoji has already been reacted to this comment."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1423 core/models.py:1423
|
||||
#: build/lib/core/models.py:1419 core/models.py:1419
|
||||
msgid "Reaction"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1424 core/models.py:1424
|
||||
#: build/lib/core/models.py:1420 core/models.py:1420
|
||||
msgid "Reactions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
#: build/lib/core/models.py:1431 core/models.py:1431
|
||||
msgid "description"
|
||||
msgstr "说明"
|
||||
|
||||
#: build/lib/core/models.py:1436 core/models.py:1436
|
||||
#: build/lib/core/models.py:1432 core/models.py:1432
|
||||
msgid "code"
|
||||
msgstr "代码"
|
||||
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1435 core/models.py:1435
|
||||
msgid "public"
|
||||
msgstr "公开"
|
||||
|
||||
#: build/lib/core/models.py:1441 core/models.py:1441
|
||||
#: build/lib/core/models.py:1437 core/models.py:1437
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "该模板是否公开供任何人使用。"
|
||||
|
||||
#: build/lib/core/models.py:1447 core/models.py:1447
|
||||
#: build/lib/core/models.py:1443 core/models.py:1443
|
||||
msgid "Template"
|
||||
msgstr "模板"
|
||||
|
||||
#: build/lib/core/models.py:1448 core/models.py:1448
|
||||
#: build/lib/core/models.py:1444 core/models.py:1444
|
||||
msgid "Templates"
|
||||
msgstr "模板"
|
||||
|
||||
#: build/lib/core/models.py:1501 core/models.py:1501
|
||||
#: build/lib/core/models.py:1497 core/models.py:1497
|
||||
msgid "Template/user relation"
|
||||
msgstr "模板/用户关系"
|
||||
|
||||
#: build/lib/core/models.py:1502 core/models.py:1502
|
||||
#: build/lib/core/models.py:1498 core/models.py:1498
|
||||
msgid "Template/user relations"
|
||||
msgstr "模板/用户关系集"
|
||||
|
||||
#: build/lib/core/models.py:1508 core/models.py:1508
|
||||
#: build/lib/core/models.py:1504 core/models.py:1504
|
||||
msgid "This user is already in this template."
|
||||
msgstr "该用户已在此模板中。"
|
||||
|
||||
#: build/lib/core/models.py:1514 core/models.py:1514
|
||||
#: build/lib/core/models.py:1510 core/models.py:1510
|
||||
msgid "This team is already in this template."
|
||||
msgstr "该团队已在此模板中。"
|
||||
|
||||
#: build/lib/core/models.py:1591 core/models.py:1591
|
||||
#: build/lib/core/models.py:1587 core/models.py:1587
|
||||
msgid "email address"
|
||||
msgstr "电子邮件地址"
|
||||
|
||||
#: build/lib/core/models.py:1610 core/models.py:1610
|
||||
#: build/lib/core/models.py:1606 core/models.py:1606
|
||||
msgid "Document invitation"
|
||||
msgstr "文档邀请"
|
||||
|
||||
#: build/lib/core/models.py:1611 core/models.py:1611
|
||||
#: build/lib/core/models.py:1607 core/models.py:1607
|
||||
msgid "Document invitations"
|
||||
msgstr "文档邀请"
|
||||
|
||||
#: build/lib/core/models.py:1631 core/models.py:1631
|
||||
#: build/lib/core/models.py:1627 core/models.py:1627
|
||||
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.4.0"
|
||||
version = "4.2.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -25,13 +25,13 @@ license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"beautifulsoup4==4.14.3",
|
||||
"boto3==1.42.17",
|
||||
"beautifulsoup4==4.14.2",
|
||||
"boto3==1.40.74",
|
||||
"Brotli==1.2.0",
|
||||
"celery[redis]==5.5.3",
|
||||
"django-configurations==2.5.1",
|
||||
"django-cors-headers==4.9.0",
|
||||
"django-countries==8.2.0",
|
||||
"django-countries==8.1.0",
|
||||
"django-csp==4.0",
|
||||
"django-filter==25.2",
|
||||
"django-lasuite[all]==0.0.22",
|
||||
@@ -39,8 +39,8 @@ dependencies = [
|
||||
"django-redis==6.0.0",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django<6.0.0",
|
||||
"django-treebeard==4.8.0",
|
||||
"django==5.2.9",
|
||||
"django-treebeard==4.7.1",
|
||||
"djangorestframework==3.16.1",
|
||||
"drf_spectacular==0.29.0",
|
||||
"dockerflow==2024.4.2",
|
||||
@@ -48,19 +48,18 @@ dependencies = [
|
||||
"factory_boy==3.3.3",
|
||||
"gunicorn==23.0.0",
|
||||
"jsonschema==4.25.1",
|
||||
"langfuse==3.11.2",
|
||||
"lxml==6.0.2",
|
||||
"markdown==3.10",
|
||||
"mozilla-django-oidc==5.0.2",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
"nested-multipart-parser==1.6.0",
|
||||
"openai==2.14.0",
|
||||
"psycopg[binary]==3.3.2",
|
||||
"pycrdt==0.12.44",
|
||||
"openai==2.8.0",
|
||||
"psycopg[binary]==3.2.12",
|
||||
"pycrdt==0.12.43",
|
||||
"PyJWT==2.10.1",
|
||||
"python-magic==0.4.27",
|
||||
"redis<6.0.0",
|
||||
"requests==2.32.5",
|
||||
"sentry-sdk==2.48.0",
|
||||
"sentry-sdk==2.44.0",
|
||||
"whitenoise==6.11.0",
|
||||
]
|
||||
|
||||
@@ -74,20 +73,20 @@ dependencies = [
|
||||
dev = [
|
||||
"django-extensions==4.1",
|
||||
"django-test-migrations==1.5.0",
|
||||
"drf-spectacular-sidecar==2025.12.1",
|
||||
"drf-spectacular-sidecar==2025.10.1",
|
||||
"freezegun==1.5.5",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==9.8.0",
|
||||
"pyfakefs==6.0.0",
|
||||
"ipython==9.7.0",
|
||||
"pyfakefs==5.10.2",
|
||||
"pylint-django==2.6.1",
|
||||
"pylint<4.0.0",
|
||||
"pytest-cov==7.0.0",
|
||||
"pytest-django==4.11.1",
|
||||
"pytest==9.0.2",
|
||||
"pytest==9.0.1",
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.8.0",
|
||||
"responses==0.25.8",
|
||||
"ruff==0.14.10",
|
||||
"ruff==0.14.5",
|
||||
"types-requests==2.32.4.20250913",
|
||||
]
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -58,7 +58,7 @@ test.describe('Doc Comments', () => {
|
||||
await page.getByRole('button', { name: '👍' }).click();
|
||||
|
||||
await expect(
|
||||
thread.getByRole('img', { name: `E2E ${browserName}` }).first(),
|
||||
thread.getByRole('img', { name: 'E2E Chromium' }).first(),
|
||||
).toBeVisible();
|
||||
await expect(thread.getByText('This is a comment').first()).toBeVisible();
|
||||
await expect(thread.getByText(`E2E ${browserName}`).first()).toBeVisible();
|
||||
@@ -394,8 +394,6 @@ test.describe('Doc Comments mobile', () => {
|
||||
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();
|
||||
|
||||
@@ -73,7 +73,7 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await page.locator('.ProseMirror').focus();
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { Download, Page, expect, test } from '@playwright/test';
|
||||
import { 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('/');
|
||||
@@ -47,14 +46,81 @@ test.describe('Doc Export', () => {
|
||||
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* 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 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);
|
||||
});
|
||||
|
||||
test('it exports the doc to docx', async ({ page, browserName }) => {
|
||||
const randomDoc = await overrideDocContent({ 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();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
@@ -77,14 +143,29 @@ 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 overrideDocContent({ page, browserName });
|
||||
const [randomDoc] = await createDoc(page, 'doc-editor-odt', browserName, 1);
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World ODT');
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Resizable image with caption').click();
|
||||
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.getByText('Upload image').click();
|
||||
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
|
||||
|
||||
const image = page
|
||||
.locator('.--docs--editor-container img.bn-visual-media')
|
||||
.first();
|
||||
|
||||
await expect(image).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
@@ -277,6 +358,108 @@ 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,
|
||||
@@ -323,18 +506,53 @@ test.describe('Doc Export', () => {
|
||||
expect(pdfString).toContain('/Lang (fr)');
|
||||
});
|
||||
|
||||
test('it exports the doc to PDF and checks regressions', async ({
|
||||
test('it exports the doc with interlinking', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
// PDF Binary comparison is different depending on the browser used
|
||||
// We only run this test on Chromium to avoid having to maintain
|
||||
// multiple sets of PDF fixtures
|
||||
if (browserName !== 'chromium') {
|
||||
test.skip();
|
||||
}
|
||||
const [randomDoc] = await createDoc(
|
||||
page,
|
||||
'export-interlinking',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
const randomDoc = await overrideDocContent({ page, browserName });
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
const { name: docChild } = await createRootSubPage(
|
||||
page,
|
||||
browserName,
|
||||
'export-interlink-child',
|
||||
);
|
||||
|
||||
await verifyDocName(page, docChild);
|
||||
|
||||
const editor = await openSuggestionMenu({ page });
|
||||
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 interlink = editor
|
||||
.locator('.--docs--interlinking-link-inline-content')
|
||||
.first();
|
||||
|
||||
await expect(interlink).toContainText(randomDoc);
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${docChild}.pdf`);
|
||||
});
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
@@ -342,159 +560,77 @@ test.describe('Doc Export', () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByTestId('doc-open-modal-download-button'),
|
||||
).toBeVisible();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
});
|
||||
|
||||
await page.getByTestId('doc-export-download-button').click();
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
expect(download.suggestedFilename()).toBe(`${docChild}.pdf`);
|
||||
|
||||
// If we need to update the PDF regression fixture, uncomment the line below
|
||||
//await savePDFToAssetFolder(download);
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfParse = new PDFParse({ data: pdfBuffer });
|
||||
const pdfText = await pdfParse.getText();
|
||||
expect(pdfText.text).toContain(randomDoc);
|
||||
});
|
||||
|
||||
// Assert the generated PDF matches "assets/doc-export-regressions.pdf"
|
||||
await comparePDFWithAssetFolder(download);
|
||||
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);
|
||||
|
||||
const editor = await openSuggestionMenu({ page });
|
||||
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 interlink = editor
|
||||
.locator('.--docs--interlinking-link-inline-content')
|
||||
.first();
|
||||
|
||||
await expect(interlink).toContainText(randomDoc);
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.getByRole('combobox', { name: 'Format' }).click();
|
||||
await page.getByRole('option', { name: 'Odt' }).click();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${docChild}.odt`);
|
||||
});
|
||||
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${docChild}.odt`);
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 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();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
return randomDoc;
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ test.describe('Inherited share accesses', () => {
|
||||
`doc-share-member-row-user.test@${browserName}.test`,
|
||||
);
|
||||
await expect(user).toBeVisible();
|
||||
await expect(user.getByText(`E2E ${browserName}`)).toBeVisible();
|
||||
await expect(user.getByText('E2E Chromium')).toBeVisible();
|
||||
await expect(user.getByText('Owner')).toBeVisible();
|
||||
|
||||
await page
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createDoc,
|
||||
expectLoginPage,
|
||||
keyCloakSignIn,
|
||||
randomName,
|
||||
updateDocTitle,
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
@@ -19,6 +20,50 @@ test.describe('Doc Tree', () => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('create new sub pages', async ({ page, browserName }) => {
|
||||
const [titleParent] = await createDoc(
|
||||
page,
|
||||
'doc-tree-content',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
await verifyDocName(page, titleParent);
|
||||
const addButton = page.getByTestId('new-doc-button');
|
||||
const docTree = page.getByTestId('doc-tree');
|
||||
|
||||
await expect(addButton).toBeVisible();
|
||||
|
||||
// Wait for and intercept the POST request to create a new page
|
||||
const responsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/documents/') &&
|
||||
response.url().includes('/children/') &&
|
||||
response.request().method() === 'POST',
|
||||
);
|
||||
|
||||
await clickOnAddRootSubPage(page);
|
||||
const response = await responsePromise;
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const subPageJson = await response.json();
|
||||
|
||||
await expect(docTree).toBeVisible();
|
||||
const subPageItem = docTree
|
||||
.getByTestId(`doc-sub-page-item-${subPageJson.id}`)
|
||||
.first();
|
||||
|
||||
await expect(subPageItem).toBeVisible();
|
||||
await subPageItem.click();
|
||||
await verifyDocName(page, '');
|
||||
const input = page.getByRole('textbox', { name: 'Document title' });
|
||||
await input.click();
|
||||
const [randomDocName] = randomName('doc-tree-test', browserName, 1);
|
||||
await input.fill(randomDocName);
|
||||
await input.press('Enter');
|
||||
await expect(subPageItem.getByText(randomDocName)).toBeVisible();
|
||||
await page.reload();
|
||||
await expect(subPageItem.getByText(randomDocName)).toBeVisible();
|
||||
});
|
||||
|
||||
test('check the reorder of sub pages', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'doc-tree-content', browserName, 1);
|
||||
const addButton = page.getByTestId('new-doc-button');
|
||||
|
||||
@@ -42,8 +42,8 @@ test.describe('Doc Version', () => {
|
||||
// Write more
|
||||
await writeInEditor({ page, text: 'It will create a version' });
|
||||
|
||||
const { suggestionMenu } = await openSuggestionMenu({ page });
|
||||
await suggestionMenu.getByText('Add a callout block').click();
|
||||
await openSuggestionMenu({ page });
|
||||
await page.getByText('Add a callout block').click();
|
||||
|
||||
const calloutBlock = page
|
||||
.locator('div[data-content-type="callout"]')
|
||||
|
||||
@@ -59,90 +59,45 @@ test.describe('Header', () => {
|
||||
).toBeVisible();
|
||||
|
||||
await expect(header.getByText('English')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
header.getByRole('button', {
|
||||
name: 'Les services de La Suite numérique',
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks a custom waffle', async ({ page }) => {
|
||||
test('checks La Gauffre interaction', async ({ page }) => {
|
||||
await overrideConfig(page, {
|
||||
theme_customization: {
|
||||
waffle: {
|
||||
data: {
|
||||
services: [
|
||||
{
|
||||
name: 'Docs E2E Custom 1',
|
||||
url: 'https://docs.numerique.gouv.fr/',
|
||||
maturity: 'stable',
|
||||
logo: 'https://lasuite.numerique.gouv.fr/assets/products/docs.svg',
|
||||
},
|
||||
{
|
||||
name: 'Docs E2E Custom 2',
|
||||
url: 'https://docs.numerique.gouv.fr/',
|
||||
maturity: 'stable',
|
||||
logo: 'https://lasuite.numerique.gouv.fr/assets/products/docs.svg',
|
||||
},
|
||||
],
|
||||
},
|
||||
showMoreLimit: 9,
|
||||
},
|
||||
},
|
||||
FRONTEND_THEME: 'dsfr',
|
||||
});
|
||||
await page.goto('/');
|
||||
|
||||
const header = page.locator('header').first();
|
||||
|
||||
await expect(
|
||||
header.getByRole('button', { name: 'Digital LaSuite services' }),
|
||||
header.getByRole('button', {
|
||||
name: 'Les services de La Suite numérique',
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
/**
|
||||
* The Waffle loads a js file from a remote server,
|
||||
* it takes some time to load the file and have the interaction available
|
||||
*/
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
await header
|
||||
.getByRole('button', { name: 'Digital LaSuite services' })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs E2E Custom 1' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs E2E Custom 2' }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks the waffle dsfr', async ({ page }) => {
|
||||
await overrideConfig(page, {
|
||||
theme_customization: {
|
||||
waffle: {
|
||||
apiUrl: 'https://lasuite.numerique.gouv.fr/api/services',
|
||||
showMoreLimit: 9,
|
||||
},
|
||||
},
|
||||
});
|
||||
await page.goto('/');
|
||||
|
||||
const header = page.locator('header').first();
|
||||
|
||||
await expect(
|
||||
header.getByRole('button', { name: 'Digital LaSuite services' }),
|
||||
).toBeVisible();
|
||||
|
||||
/**
|
||||
* The Waffle loads a js file from a remote server,
|
||||
* La gaufre load a js file from a remote server,
|
||||
* it takes some time to load the file and have the interaction available
|
||||
*/
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
await header
|
||||
.getByRole('button', {
|
||||
name: 'Digital LaSuite services',
|
||||
name: 'Les services de La Suite numérique',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(page.getByRole('link', { name: 'Tchap' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'France Transfert' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('link', { name: 'Grist' })).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: 'Visio' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,6 +124,11 @@ test.describe('Header mobile', () => {
|
||||
|
||||
await expect(header.getByLabel('Open the header menu')).toBeVisible();
|
||||
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
|
||||
await expect(
|
||||
header.getByRole('button', {
|
||||
name: 'Les services de La Suite numérique',
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -113,6 +113,9 @@ test.describe('Home page', () => {
|
||||
});
|
||||
await expect(languageButton).toBeVisible();
|
||||
|
||||
await expect(
|
||||
header.getByRole('button', { name: 'Les services de La Suite numé' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
header.getByRole('img', { name: 'Gouvernement Logo' }),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
TestLanguage,
|
||||
createDoc,
|
||||
overrideConfig,
|
||||
waitForLanguageSwitch,
|
||||
} from './utils-common';
|
||||
import { TestLanguage, createDoc, waitForLanguageSwitch } from './utils-common';
|
||||
import { openSuggestionMenu } from './utils-editor';
|
||||
|
||||
test.describe('Language', () => {
|
||||
@@ -112,21 +107,10 @@ test.describe('Language', () => {
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await overrideConfig(page, {
|
||||
LANGUAGES: [
|
||||
['en-us', 'English'],
|
||||
['fr-fr', 'Français'],
|
||||
['sv-se', 'Svenska'],
|
||||
],
|
||||
LANGUAGE_CODE: 'en-us',
|
||||
});
|
||||
|
||||
await createDoc(page, 'doc-toolbar', browserName, 1);
|
||||
|
||||
const { editor, suggestionMenu } = await openSuggestionMenu({ page });
|
||||
await expect(
|
||||
suggestionMenu.getByText('Headings', { exact: true }),
|
||||
).toBeVisible();
|
||||
const editor = await openSuggestionMenu({ page });
|
||||
await expect(page.getByText('Headings', { exact: true })).toBeVisible();
|
||||
|
||||
await editor.click(); // close the menu
|
||||
|
||||
@@ -137,17 +121,6 @@ test.describe('Language', () => {
|
||||
|
||||
// Trigger slash menu to show french menu
|
||||
await openSuggestionMenu({ page });
|
||||
await expect(
|
||||
suggestionMenu.getByText('Titres', { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
/**
|
||||
* Swedish is not yet supported in the BlockNote locales, so it should fallback to English
|
||||
*/
|
||||
await waitForLanguageSwitch(page, TestLanguage.Swedish);
|
||||
await openSuggestionMenu({ page });
|
||||
await expect(
|
||||
suggestionMenu.getByText('Headings', { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText('Titres', { exact: true })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -224,9 +224,7 @@ export const updateDocTitle = async (page: Page, title: string) => {
|
||||
await expect(input).toHaveText('');
|
||||
await expect(input).toBeVisible();
|
||||
await input.click();
|
||||
await input.fill(title, {
|
||||
force: true,
|
||||
});
|
||||
await input.fill(title);
|
||||
await input.click();
|
||||
await input.blur();
|
||||
await verifyDocName(page, title);
|
||||
@@ -330,10 +328,6 @@ export const TestLanguage = {
|
||||
label: 'Deutsch',
|
||||
expectedLocale: ['de-de'],
|
||||
},
|
||||
Swedish: {
|
||||
label: 'Svenska',
|
||||
expectedLocale: ['sv-se'],
|
||||
},
|
||||
} as const;
|
||||
|
||||
type TestLanguageKey = keyof typeof TestLanguage;
|
||||
|
||||
@@ -7,11 +7,11 @@ export const getEditor = async ({ page }: { page: Page }) => {
|
||||
};
|
||||
|
||||
export const openSuggestionMenu = async ({ page }: { page: Page }) => {
|
||||
const editor = await writeInEditor({ page, text: '/' });
|
||||
const editor = await getEditor({ page });
|
||||
await editor.click();
|
||||
await writeInEditor({ page, text: '/' });
|
||||
|
||||
const suggestionMenu = page.locator('.bn-suggestion-menu');
|
||||
|
||||
return { editor, suggestionMenu };
|
||||
return editor;
|
||||
};
|
||||
|
||||
export const writeInEditor = async ({
|
||||
@@ -22,11 +22,6 @@ export const writeInEditor = async ({
|
||||
text: string;
|
||||
}) => {
|
||||
const editor = await getEditor({ page });
|
||||
await editor
|
||||
.locator('.bn-block-outer:last-child')
|
||||
.last()
|
||||
.locator('.bn-inline-content:last-child')
|
||||
.last()
|
||||
.fill(text);
|
||||
await editor.locator('.bn-block-outer .bn-inline-content').last().fill(text);
|
||||
return editor;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "4.4.0",
|
||||
"version": "4.2.0",
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"author": "DINUM",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,53 +1,58 @@
|
||||
import {
|
||||
dsfrGlobals,
|
||||
getUIKitThemesFromGlobals,
|
||||
whiteLabelGlobals,
|
||||
} from '@gouvfr-lasuite/ui-kit';
|
||||
import { cunninghamConfig as tokens } from '@gouvfr-lasuite/ui-kit';
|
||||
import { defaultTokens } from '@openfun/cunningham-react';
|
||||
import merge from 'lodash/merge';
|
||||
|
||||
const themeWhiteLabelLight = getUIKitThemesFromGlobals(whiteLabelGlobals, {
|
||||
prefix: 'default',
|
||||
variants: ['light'],
|
||||
overrides: {
|
||||
globals: {
|
||||
spacing: {
|
||||
'0': '0rem',
|
||||
none: '0rem',
|
||||
auto: 'auto',
|
||||
bx: '2.2rem',
|
||||
full: '100%',
|
||||
'3xs': '0.25rem',
|
||||
'2xs': '0.375rem',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
logo: {
|
||||
src: '',
|
||||
alt: '',
|
||||
widthHeader: '',
|
||||
widthFooter: '',
|
||||
},
|
||||
'home-proconnect': false,
|
||||
icon: {
|
||||
src: '/assets/icon-docs.svg',
|
||||
width: '32px',
|
||||
height: 'auto',
|
||||
},
|
||||
favicon: {
|
||||
'png-light': '/assets/favicon-light.png',
|
||||
'png-dark': '/assets/favicon-dark.png',
|
||||
// Uikit does not provide the full list of tokens.
|
||||
// To be able to override correctly, we need to merge with the default tokens.
|
||||
let mergedColors = merge(
|
||||
defaultTokens.globals.colors,
|
||||
tokens.themes.default.globals.colors,
|
||||
);
|
||||
|
||||
mergedColors = {
|
||||
...mergedColors,
|
||||
'logo-1': '#2845C1',
|
||||
};
|
||||
|
||||
tokens.themes.default.globals = {
|
||||
...tokens.themes.default.globals,
|
||||
...{
|
||||
colors: mergedColors,
|
||||
font: {
|
||||
...tokens.themes.default.globals.font,
|
||||
families: {
|
||||
base: 'sans-serif',
|
||||
accent: 'sans-serif',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const themeDefault = {
|
||||
default: themeWhiteLabelLight['default-light'],
|
||||
};
|
||||
|
||||
const themesDSFRLight = getUIKitThemesFromGlobals(dsfrGlobals, {
|
||||
prefix: 'dsfr',
|
||||
variants: ['light'],
|
||||
overrides: {
|
||||
tokens.themes.default.components = {
|
||||
...tokens.themes.default.components,
|
||||
...{
|
||||
logo: {
|
||||
src: '',
|
||||
alt: '',
|
||||
widthHeader: '',
|
||||
widthFooter: '',
|
||||
},
|
||||
'la-gaufre': false,
|
||||
'home-proconnect': false,
|
||||
icon: {
|
||||
src: '/assets/icon-docs.svg',
|
||||
width: '32px',
|
||||
height: 'auto',
|
||||
},
|
||||
favicon: {
|
||||
'png-light': '/assets/favicon-light.png',
|
||||
'png-dark': '/assets/favicon-dark.png',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const dsfrTheme = {
|
||||
dsfr: {
|
||||
globals: {
|
||||
font: {
|
||||
families: {
|
||||
@@ -63,6 +68,7 @@ const themesDSFRLight = getUIKitThemesFromGlobals(dsfrGlobals, {
|
||||
widthFooter: '220px',
|
||||
alt: 'Gouvernement Logo',
|
||||
},
|
||||
'la-gaufre': true,
|
||||
'home-proconnect': true,
|
||||
icon: {
|
||||
src: '/assets/icon-docs-dsfr.svg',
|
||||
@@ -76,16 +82,317 @@ const themesDSFRLight = getUIKitThemesFromGlobals(dsfrGlobals, {
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const themesDSFR = {
|
||||
dsfr: themesDSFRLight['dsfr-light'],
|
||||
const genericTheme = {
|
||||
generic: {
|
||||
globals: {
|
||||
colors: {
|
||||
'brand-050': '#EEF1FA',
|
||||
'brand-100': '#DDE2F5',
|
||||
'brand-150': '#CED3F1',
|
||||
'brand-200': '#BEC5F0',
|
||||
'brand-250': '#AFB5F1',
|
||||
'brand-300': '#A0A5F6',
|
||||
'brand-350': '#8F94FD',
|
||||
'brand-400': '#8184FC',
|
||||
'brand-450': '#7576EE',
|
||||
'brand-500': '#6969DF',
|
||||
'brand-550': '#5E5CD0',
|
||||
'brand-600': '#534FC2',
|
||||
'brand-650': '#4844AD',
|
||||
'brand-700': '#3E3B98',
|
||||
'brand-750': '#36347D',
|
||||
'brand-800': '#2D2F5F',
|
||||
'brand-850': '#262848',
|
||||
'brand-900': '#1C1E32',
|
||||
'brand-950': '#11131F',
|
||||
'gray-000': '#FFFFFF',
|
||||
'gray-025': '#F8F8F9',
|
||||
'gray-050': '#F0F0F3',
|
||||
'gray-100': '#E2E2EA',
|
||||
'gray-150': '#D3D4E0',
|
||||
'gray-200': '#C5C6D5',
|
||||
'gray-250': '#B7B7CB',
|
||||
'gray-300': '#A9A9BF',
|
||||
'gray-350': '#9C9CB2',
|
||||
'gray-400': '#8F8FA4',
|
||||
'gray-450': '#828297',
|
||||
'gray-500': '#75758A',
|
||||
'gray-550': '#69697D',
|
||||
'gray-600': '#5D5D70',
|
||||
'gray-650': '#515164',
|
||||
'gray-700': '#454558',
|
||||
'gray-750': '#3A3A4C',
|
||||
'gray-800': '#2F303D',
|
||||
'gray-850': '#25252F',
|
||||
'gray-900': '#1B1B23',
|
||||
'gray-950': '#111114',
|
||||
'gray-1000': '#000000',
|
||||
'info-050': '#EAF2F9',
|
||||
'info-100': '#D5E4F3',
|
||||
'info-150': '#BFD7F0',
|
||||
'info-200': '#A7CAEE',
|
||||
'info-250': '#8DBDEF',
|
||||
'info-300': '#6EB0F2',
|
||||
'info-350': '#50A2F5',
|
||||
'info-400': '#3593F4',
|
||||
'info-450': '#1185ED',
|
||||
'info-500': '#0077DE',
|
||||
'info-550': '#0069CF',
|
||||
'info-600': '#005BC0',
|
||||
'info-650': '#0D4EAA',
|
||||
'info-700': '#124394',
|
||||
'info-750': '#163878',
|
||||
'info-800': '#192F5A',
|
||||
'info-850': '#192541',
|
||||
'info-900': '#141B2D',
|
||||
'info-950': '#0C111C',
|
||||
'success-050': '#E8F1EA',
|
||||
'success-100': '#CFE4D4',
|
||||
'success-150': '#BAD9C1',
|
||||
'success-200': '#A2CFAD',
|
||||
'success-250': '#86C597',
|
||||
'success-300': '#6CBA83',
|
||||
'success-350': '#4FB070',
|
||||
'success-400': '#40A363',
|
||||
'success-450': '#309556',
|
||||
'success-500': '#1E884A',
|
||||
'success-550': '#027B3E',
|
||||
'success-600': '#016D31',
|
||||
'success-650': '#006024',
|
||||
'success-700': '#005317',
|
||||
'success-750': '#0D4511',
|
||||
'success-800': '#11380E',
|
||||
'success-850': '#132A11',
|
||||
'success-900': '#101E0F',
|
||||
'success-950': '#091209',
|
||||
'warning-050': '#F8F0E9',
|
||||
'warning-100': '#F1E0D3',
|
||||
'warning-150': '#ECD0BC',
|
||||
'warning-200': '#E8C0A4',
|
||||
'warning-250': '#E8AE8A',
|
||||
'warning-300': '#EB9970',
|
||||
'warning-350': '#E98456',
|
||||
'warning-400': '#E57036',
|
||||
'warning-450': '#DA5E18',
|
||||
'warning-500': '#CB5000',
|
||||
'warning-550': '#BC4200',
|
||||
'warning-600': '#AD3300',
|
||||
'warning-650': '#9E2300',
|
||||
'warning-700': '#882011',
|
||||
'warning-750': '#731E16',
|
||||
'warning-800': '#58201A',
|
||||
'warning-850': '#401D18',
|
||||
'warning-900': '#2E1714',
|
||||
'warning-950': '#1D0F0D',
|
||||
'error-050': '#F9EFEC',
|
||||
'error-100': '#F4DFD9',
|
||||
'error-150': '#F0CEC6',
|
||||
'error-200': '#EEBCB2',
|
||||
'error-250': '#EEA99D',
|
||||
'error-300': '#EF9486',
|
||||
'error-350': '#F37C6E',
|
||||
'error-400': '#F65F53',
|
||||
'error-450': '#F0463D',
|
||||
'error-500': '#E82322',
|
||||
'error-550': '#D7010E',
|
||||
'error-600': '#C00100',
|
||||
'error-650': '#AA0000',
|
||||
'error-700': '#910C06',
|
||||
'error-750': '#731E16',
|
||||
'error-800': '#58201A',
|
||||
'error-850': '#401D18',
|
||||
'error-900': '#2E1714',
|
||||
'error-950': '#1D0F0D',
|
||||
'red-050': '#FAEFEE',
|
||||
'red-100': '#F4DEDD',
|
||||
'red-150': '#F1CDCB',
|
||||
'red-200': '#EFBBBA',
|
||||
'red-250': '#EEA8A8',
|
||||
'red-300': '#F09394',
|
||||
'red-350': '#F37B7E',
|
||||
'red-400': '#EF6569',
|
||||
'red-450': '#E94A55',
|
||||
'red-500': '#DA3B49',
|
||||
'red-550': '#CA2A3C',
|
||||
'red-600': '#BB1330',
|
||||
'red-650': '#A90021',
|
||||
'red-700': '#910A13',
|
||||
'red-750': '#731E16',
|
||||
'red-800': '#58201A',
|
||||
'red-850': '#411D18',
|
||||
'red-900': '#2E1714',
|
||||
'red-950': '#1D0F0D',
|
||||
'orange-050': '#F8F0E9',
|
||||
'orange-100': '#F1E0D3',
|
||||
'orange-150': '#ECD0BD',
|
||||
'orange-200': '#EABFA6',
|
||||
'orange-250': '#EBAC90',
|
||||
'orange-300': '#EC9772',
|
||||
'orange-350': '#E5845A',
|
||||
'orange-400': '#D6774D',
|
||||
'orange-450': '#C86A40',
|
||||
'orange-500': '#B95D33',
|
||||
'orange-550': '#AB5025',
|
||||
'orange-600': '#9D4315',
|
||||
'orange-650': '#8F3600',
|
||||
'orange-700': '#812900',
|
||||
'orange-750': '#6C2511',
|
||||
'orange-800': '#572017',
|
||||
'orange-850': '#401D18',
|
||||
'orange-900': '#2E1714',
|
||||
'orange-950': '#1D0F0D',
|
||||
'brown-050': '#F6F0E8',
|
||||
'brown-100': '#F1E0D3',
|
||||
'brown-150': '#EBD0BA',
|
||||
'brown-200': '#E2C0A6',
|
||||
'brown-250': '#D4B398',
|
||||
'brown-300': '#C6A58B',
|
||||
'brown-350': '#B8987E',
|
||||
'brown-400': '#AA8B71',
|
||||
'brown-450': '#9D7E65',
|
||||
'brown-500': '#8F7158',
|
||||
'brown-550': '#82654C',
|
||||
'brown-600': '#765841',
|
||||
'brown-650': '#694C35',
|
||||
'brown-700': '#5D412A',
|
||||
'brown-750': '#51361E',
|
||||
'brown-800': '#452A13',
|
||||
'brown-850': '#392008',
|
||||
'brown-900': '#29180A',
|
||||
'brown-950': '#1B0F08',
|
||||
'yellow-050': '#F3F0E7',
|
||||
'yellow-100': '#E9E2CF',
|
||||
'yellow-150': '#E1D4B7',
|
||||
'yellow-200': '#D9C599',
|
||||
'yellow-250': '#D2B677',
|
||||
'yellow-300': '#CAA756',
|
||||
'yellow-350': '#C2972E',
|
||||
'yellow-400': '#B98900',
|
||||
'yellow-450': '#AB7B00',
|
||||
'yellow-500': '#9D6E00',
|
||||
'yellow-550': '#916100',
|
||||
'yellow-600': '#855400',
|
||||
'yellow-650': '#784700',
|
||||
'yellow-700': '#6C3A00',
|
||||
'yellow-750': '#5F2E00',
|
||||
'yellow-800': '#512302',
|
||||
'yellow-850': '#3E1D10',
|
||||
'yellow-900': '#2D1711',
|
||||
'yellow-950': '#1D0F0D',
|
||||
'green-050': '#E6F1E9',
|
||||
'green-100': '#CFE4D5',
|
||||
'green-150': '#B8D8C1',
|
||||
'green-200': '#A0CFAE',
|
||||
'green-250': '#84C59A',
|
||||
'green-300': '#65BA86',
|
||||
'green-350': '#45B173',
|
||||
'green-400': '#23A562',
|
||||
'green-450': '#029755',
|
||||
'green-500': '#008948',
|
||||
'green-550': '#017B3B',
|
||||
'green-600': '#006E2E',
|
||||
'green-650': '#006022',
|
||||
'green-700': '#005314',
|
||||
'green-750': '#0D4510',
|
||||
'green-800': '#11380E',
|
||||
'green-850': '#132A11',
|
||||
'green-900': '#101E0F',
|
||||
'green-950': '#091209',
|
||||
'blue1-050': '#EBF1F9',
|
||||
'blue1-100': '#D6E4F4',
|
||||
'blue1-150': '#C1D7F0',
|
||||
'blue1-200': '#AACAEF',
|
||||
'blue1-250': '#8FBCEF',
|
||||
'blue1-300': '#7CAFEB',
|
||||
'blue1-350': '#68A1E4',
|
||||
'blue1-400': '#5B94D6',
|
||||
'blue1-450': '#4E86C7',
|
||||
'blue1-500': '#4279B9',
|
||||
'blue1-550': '#356CAC',
|
||||
'blue1-600': '#28609E',
|
||||
'blue1-650': '#1B5390',
|
||||
'blue1-700': '#0B4783',
|
||||
'blue1-750': '#0F3C6E',
|
||||
'blue1-800': '#133059',
|
||||
'blue1-850': '#152641',
|
||||
'blue1-900': '#121C2D',
|
||||
'blue1-950': '#0B111C',
|
||||
'blue2-050': '#E7F3F4',
|
||||
'blue2-100': '#CEE7E9',
|
||||
'blue2-150': '#B2DCE0',
|
||||
'blue2-200': '#91D1D7',
|
||||
'blue2-250': '#68C7D0',
|
||||
'blue2-300': '#43BBC5',
|
||||
'blue2-350': '#00AFBA',
|
||||
'blue2-400': '#01A0AA',
|
||||
'blue2-450': '#00929D',
|
||||
'blue2-500': '#00848F',
|
||||
'blue2-550': '#007682',
|
||||
'blue2-600': '#016874',
|
||||
'blue2-650': '#005B67',
|
||||
'blue2-700': '#004E5A',
|
||||
'blue2-750': '#00424E',
|
||||
'blue2-800': '#003642',
|
||||
'blue2-850': '#002A38',
|
||||
'blue2-900': '#061E28',
|
||||
'blue2-950': '#071219',
|
||||
'purple-050': '#F7F0F6',
|
||||
'purple-100': '#EEE0EE',
|
||||
'purple-150': '#E7D1E7',
|
||||
'purple-200': '#DBBFE4',
|
||||
'purple-250': '#D3AEE2',
|
||||
'purple-300': '#CB99E1',
|
||||
'purple-350': '#C188D9',
|
||||
'purple-400': '#B47BCB',
|
||||
'purple-450': '#A66EBD',
|
||||
'purple-500': '#9961AF',
|
||||
'purple-550': '#8B55A1',
|
||||
'purple-600': '#7E4894',
|
||||
'purple-650': '#723C87',
|
||||
'purple-700': '#633376',
|
||||
'purple-750': '#552A65',
|
||||
'purple-800': '#452551',
|
||||
'purple-850': '#35213D',
|
||||
'purple-900': '#261A2C',
|
||||
'purple-950': '#17111C',
|
||||
'pink-050': '#F8EFF4',
|
||||
'pink-100': '#F0DFEA',
|
||||
'pink-150': '#EACEDF',
|
||||
'pink-200': '#E9BBD1',
|
||||
'pink-250': '#E9A7C2',
|
||||
'pink-300': '#E095B4',
|
||||
'pink-350': '#D685A8',
|
||||
'pink-400': '#C7799B',
|
||||
'pink-450': '#B86C8D',
|
||||
'pink-500': '#AA5F80',
|
||||
'pink-550': '#9C5374',
|
||||
'pink-600': '#8E4767',
|
||||
'pink-650': '#813B5B',
|
||||
'pink-700': '#732E4F',
|
||||
'pink-750': '#632643',
|
||||
'pink-800': '#521F38',
|
||||
'pink-850': '#3E1C2B',
|
||||
'pink-900': '#2D171F',
|
||||
'pink-950': '#1C0E12',
|
||||
},
|
||||
font: {
|
||||
families: {
|
||||
base: 'Inter, Roboto Flex Variable, sans-serif',
|
||||
accent: 'Inter, Roboto Flex Variable, sans-serif',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const docsTokens = {
|
||||
...tokens,
|
||||
themes: {
|
||||
...themeDefault,
|
||||
...themesDSFR,
|
||||
...tokens.themes,
|
||||
...dsfrTheme,
|
||||
...genericTheme,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "4.4.0",
|
||||
"version": "4.2.0",
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"author": "DINUM",
|
||||
"license": "MIT",
|
||||
@@ -34,15 +34,15 @@
|
||||
"@fontsource-variable/inter": "5.2.8",
|
||||
"@fontsource-variable/material-symbols-outlined": "5.2.30",
|
||||
"@fontsource/material-icons": "5.2.7",
|
||||
"@gouvfr-lasuite/cunningham-react": "4.1.0",
|
||||
"@gouvfr-lasuite/integration": "1.0.3",
|
||||
"@gouvfr-lasuite/ui-kit": "0.18.6",
|
||||
"@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.32.1",
|
||||
"@tanstack/react-query": "5.90.16",
|
||||
"@sentry/nextjs": "10.30.0",
|
||||
"@tanstack/react-query": "5.90.12",
|
||||
"@tiptap/extensions": "*",
|
||||
"canvg": "4.0.3",
|
||||
"clsx": "2.1.1",
|
||||
@@ -52,33 +52,32 @@
|
||||
"emoji-datasource-apple": "16.0.0",
|
||||
"emoji-mart": "5.6.0",
|
||||
"emoji-regex": "10.6.0",
|
||||
"i18next": "25.7.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.312.0",
|
||||
"posthog-js": "1.306.1",
|
||||
"react": "*",
|
||||
"react-aria-components": "1.14.0",
|
||||
"react-aria-components": "1.13.0",
|
||||
"react-dom": "*",
|
||||
"react-i18next": "16.5.1",
|
||||
"react-i18next": "16.5.0",
|
||||
"react-intersection-observer": "10.0.0",
|
||||
"react-resizable-panels": "3.0.6",
|
||||
"react-select": "5.10.2",
|
||||
"styled-components": "6.1.19",
|
||||
"use-debounce": "10.0.6",
|
||||
"uuid": "13.0.0",
|
||||
"y-protocols": "1.0.7",
|
||||
"y-protocols": "1.0.6",
|
||||
"yjs": "*",
|
||||
"zustand": "5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@tanstack/react-query-devtools": "5.91.2",
|
||||
"@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.1",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/lodash": "4.17.21",
|
||||
"@types/luxon": "3.7.1",
|
||||
@@ -91,16 +90,16 @@
|
||||
"dotenv": "17.2.3",
|
||||
"eslint-plugin-docs": "*",
|
||||
"fetch-mock": "9.11.0",
|
||||
"jsdom": "27.4.0",
|
||||
"jsdom": "27.3.0",
|
||||
"node-fetch": "2.7.0",
|
||||
"prettier": "3.7.4",
|
||||
"stylelint": "16.26.1",
|
||||
"stylelint-config-standard": "39.0.1",
|
||||
"stylelint-prettier": "5.0.3",
|
||||
"typescript": "*",
|
||||
"vite-tsconfig-paths": "6.0.3",
|
||||
"vitest": "4.0.16",
|
||||
"webpack": "5.104.1",
|
||||
"vite-tsconfig-paths": "6.0.1",
|
||||
"vitest": "4.0.15",
|
||||
"webpack": "5.103.0",
|
||||
"workbox-webpack-plugin": "7.1.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { InView } from 'react-intersection-observer';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Loader } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, BoxProps } from './Box';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Alert, VariantType } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Alert, VariantType } from '@openfun/cunningham-react';
|
||||
import { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@@ -3,8 +3,6 @@ export * from './BoxButton';
|
||||
export * from './Card';
|
||||
export * from './DropButton';
|
||||
export * from './dropdown-menu/DropdownMenu';
|
||||
export * from './Emoji/EmojiPicker';
|
||||
export { default as emojidata } from './Emoji/initEmojiCallout';
|
||||
export * from './quick-search';
|
||||
export * from './Icon';
|
||||
export * from './InfiniteScroll';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Button, Modal, ModalSize } from '@openfun/cunningham-react';
|
||||
import { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, type ButtonProps } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Button, type ButtonProps } from '@openfun/cunningham-react';
|
||||
import React from 'react';
|
||||
|
||||
import { Icon } from '@/components';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Modal, ModalSize } from '@openfun/cunningham-react';
|
||||
import { ComponentPropsWithRef, PropsWithChildren } from 'react';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Loader } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { Command } from 'cmdk';
|
||||
import { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CunninghamProvider } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { CunninghamProvider } from '@openfun/cunningham-react';
|
||||
import {
|
||||
MutationCache,
|
||||
QueryClient,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Loader } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import Head from 'next/head';
|
||||
import Script from 'next/script';
|
||||
import { PropsWithChildren, useEffect, useRef } from 'react';
|
||||
|
||||
@@ -4,14 +4,13 @@ import { Resource } from 'i18next';
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { Theme } from '@/cunningham/';
|
||||
import { FooterType } from '@/features/footer';
|
||||
import { HeaderType, WaffleType } from '@/features/header';
|
||||
import { HeaderType } from '@/features/header';
|
||||
import { PostHogConf } from '@/services';
|
||||
|
||||
interface ThemeCustomization {
|
||||
footer?: FooterType;
|
||||
translations?: Resource;
|
||||
header?: HeaderType;
|
||||
waffle?: WaffleType;
|
||||
}
|
||||
|
||||
export interface ConfigResponse {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,7 @@ const getMergedTokens = (theme: Theme) => {
|
||||
return merge({}, tokens.themes['default'], tokens.themes[theme]);
|
||||
};
|
||||
|
||||
const DEFAULT_THEME: Theme = 'default';
|
||||
const DEFAULT_THEME: Theme = 'generic';
|
||||
const defaultTokens = getMergedTokens(DEFAULT_THEME);
|
||||
|
||||
const initialState: ThemeStore = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
|
||||
@@ -93,10 +93,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
|
||||
useSaveDoc(doc.id, provider.document, isConnectedToCollabServer);
|
||||
const { i18n } = useTranslation();
|
||||
let lang = i18n.resolvedLanguage;
|
||||
if (!lang || !(lang in locales)) {
|
||||
lang = 'en';
|
||||
}
|
||||
const lang = i18n.resolvedLanguage;
|
||||
|
||||
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
|
||||
|
||||
@@ -262,6 +259,7 @@ export const BlockNoteReader = ({
|
||||
const { user } = useAuth();
|
||||
const { setEditor } = useEditorStore();
|
||||
const { threadStore } = useComments(docId, false, user);
|
||||
const { t } = useTranslation();
|
||||
const editor = useCreateBlockNote(
|
||||
{
|
||||
collaboration: {
|
||||
@@ -307,6 +305,7 @@ export const BlockNoteReader = ({
|
||||
editor={editor}
|
||||
editable={false}
|
||||
theme="light"
|
||||
aria-label={t('Document viewer')}
|
||||
formattingToolbar={false}
|
||||
slashMenu={false}
|
||||
comments={false}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Loader,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@gouvfr-lasuite/cunningham-react';
|
||||
} from '@openfun/cunningham-react';
|
||||
import { PropsWithChildren, ReactNode, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Button, Modal, ModalSize } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Box, Loading } from '@/components';
|
||||
import { DocHeader } from '@/docs/doc-header/';
|
||||
import {
|
||||
Doc,
|
||||
useDocFocusManagement,
|
||||
useIsCollaborativeEditable,
|
||||
useProviderStore,
|
||||
} from '@/docs/doc-management';
|
||||
@@ -83,9 +82,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||
const readOnly =
|
||||
!doc.abilities.partial_update || !isEditable || isLoading || isDeletedDoc;
|
||||
const { setIsSkeletonVisible } = useSkeletonStore();
|
||||
const isProviderReady = Boolean(isReady && provider);
|
||||
|
||||
useDocFocusManagement(doc.id, isProviderReady);
|
||||
const isProviderReady = isReady && provider;
|
||||
|
||||
useEffect(() => {
|
||||
if (isProviderReady) {
|
||||
|
||||
@@ -5,14 +5,11 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box } from '@/components';
|
||||
|
||||
export const PICKER_HEIGHT = 500;
|
||||
|
||||
interface EmojiPickerProps {
|
||||
emojiData: EmojiMartData;
|
||||
onClickOutside: () => void;
|
||||
onEmojiSelect: ({ native }: { native: string }) => void;
|
||||
withOverlay?: boolean;
|
||||
onEscape?: () => void;
|
||||
}
|
||||
|
||||
export const EmojiPicker = ({
|
||||
@@ -20,26 +17,14 @@ export const EmojiPicker = ({
|
||||
onClickOutside,
|
||||
onEmojiSelect,
|
||||
withOverlay = false,
|
||||
onEscape,
|
||||
}: EmojiPickerProps) => {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'Escape') {
|
||||
if (onEscape) {
|
||||
onEscape();
|
||||
} else {
|
||||
onClickOutside();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const pickerContent = (
|
||||
<Box $position="absolute" $zIndex={1000} onKeyDownCapture={handleKeyDown}>
|
||||
<Box $position="absolute" $zIndex={1000} $margin="2rem 0 0 0">
|
||||
<Picker
|
||||
data={emojiData}
|
||||
locale={i18n.resolvedLanguage}
|
||||
autoFocus
|
||||
onClickOutside={onClickOutside}
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
previewPosition="none"
|
||||
@@ -212,17 +212,6 @@ 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();
|
||||
};
|
||||
|
||||
@@ -252,7 +241,6 @@ export class DocsThreadStore extends ThreadStore {
|
||||
this.upsertClientThreadData(threadData);
|
||||
this.notifySubscribers();
|
||||
this.ping(threadData.id);
|
||||
|
||||
return threadData;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,9 +12,12 @@ import { TFunction } from 'i18next';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { createGlobalStyle, css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, EmojiPicker, Icon, emojidata } from '@/components';
|
||||
import { Box, BoxButton, Icon } from '@/components';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../../types';
|
||||
import { EmojiPicker } from '../EmojiPicker';
|
||||
|
||||
import emojidata from './initEmojiCallout';
|
||||
|
||||
const CalloutBlockStyle = createGlobalStyle`
|
||||
.bn-block-content[data-content-type="callout"][data-background-color] {
|
||||
@@ -94,15 +97,15 @@ const CalloutComponent = ({
|
||||
`}
|
||||
>
|
||||
<CalloutBlockStyle />
|
||||
<Box
|
||||
$position="relative"
|
||||
$css={css`
|
||||
align-self: start;
|
||||
`}
|
||||
>
|
||||
<Box $position="relative">
|
||||
<BoxButton
|
||||
contentEditable={false}
|
||||
onClick={toggleEmojiPicker}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape' && openEmojiPicker) {
|
||||
setOpenEmojiPicker(false);
|
||||
}
|
||||
}}
|
||||
$css={css`
|
||||
font-size: 1.125rem;
|
||||
cursor: ${isEditable ? 'pointer' : 'default'};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './AccessibleImageBlock';
|
||||
export * from './CalloutBlock';
|
||||
export { default as emojidata } from './initEmojiCallout';
|
||||
export * from './PdfBlock';
|
||||
export * from './UploadLoaderBlock';
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import {
|
||||
PartialCustomInlineContentFromConfig,
|
||||
StyleSchema,
|
||||
} from '@blocknote/core';
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { createReactInlineContentSpec } from '@blocknote/react';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
|
||||
import { BoxButton, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import SelectedPageIcon from '@/docs/doc-editor/assets/doc-selected.svg';
|
||||
import { getEmojiAndTitle, useDoc } from '@/docs/doc-management/';
|
||||
import { getEmojiAndTitle, useDoc } from '@/docs/doc-management';
|
||||
|
||||
export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
|
||||
{
|
||||
type: 'interlinkingLinkInline',
|
||||
propSchema: {
|
||||
url: {
|
||||
default: '',
|
||||
},
|
||||
docId: {
|
||||
default: '',
|
||||
},
|
||||
@@ -29,97 +27,46 @@ export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
|
||||
},
|
||||
{
|
||||
render: ({ editor, inlineContent, updateInlineContent }) => {
|
||||
if (!inlineContent.props.docId) {
|
||||
return null;
|
||||
}
|
||||
const { data: doc } = useDoc({ id: inlineContent.props.docId });
|
||||
const isEditable = editor.isEditable;
|
||||
|
||||
/**
|
||||
* Should not happen
|
||||
* Update the content title if the referenced doc title changes
|
||||
*/
|
||||
if (!uuidValidate(inlineContent.props.docId)) {
|
||||
Sentry.captureException(
|
||||
new Error(`Invalid docId: ${inlineContent.props.docId}`),
|
||||
{
|
||||
extra: { info: 'InterlinkingLinkInlineContent' },
|
||||
},
|
||||
);
|
||||
useEffect(() => {
|
||||
if (
|
||||
isEditable &&
|
||||
doc?.title &&
|
||||
doc.title !== inlineContent.props.title
|
||||
) {
|
||||
updateInlineContent({
|
||||
type: 'interlinkingLinkInline',
|
||||
props: {
|
||||
...inlineContent.props,
|
||||
title: doc.title,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateInlineContent({
|
||||
type: 'interlinkingLinkInline',
|
||||
props: {
|
||||
docId: '',
|
||||
title: '',
|
||||
},
|
||||
});
|
||||
/**
|
||||
* ⚠️ When doing collaborative editing, doc?.title might be out of sync
|
||||
* causing an infinite loop of updates.
|
||||
* To prevent this, we only run this effect when doc?.title changes,
|
||||
* not when inlineContent.props.title changes.
|
||||
*/
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [doc?.title, isEditable]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkSelected
|
||||
docId={inlineContent.props.docId}
|
||||
title={inlineContent.props.title}
|
||||
isEditable={editor.isEditable}
|
||||
updateInlineContent={updateInlineContent}
|
||||
/>
|
||||
);
|
||||
return <LinkSelected {...inlineContent.props} />;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface LinkSelectedProps {
|
||||
docId: string;
|
||||
url: string;
|
||||
title: string;
|
||||
isEditable: boolean;
|
||||
updateInlineContent: (
|
||||
update: PartialCustomInlineContentFromConfig<
|
||||
{
|
||||
readonly type: 'interlinkingLinkInline';
|
||||
readonly propSchema: {
|
||||
readonly docId: {
|
||||
readonly default: '';
|
||||
};
|
||||
readonly title: {
|
||||
readonly default: '';
|
||||
};
|
||||
};
|
||||
readonly content: 'none';
|
||||
},
|
||||
StyleSchema
|
||||
>,
|
||||
) => void;
|
||||
}
|
||||
export const LinkSelected = ({
|
||||
docId,
|
||||
title,
|
||||
isEditable,
|
||||
updateInlineContent,
|
||||
}: LinkSelectedProps) => {
|
||||
const { data: doc } = useDoc({ id: docId });
|
||||
|
||||
/**
|
||||
* Update the content title if the referenced doc title changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (isEditable && doc?.title && doc.title !== title) {
|
||||
updateInlineContent({
|
||||
type: 'interlinkingLinkInline',
|
||||
props: {
|
||||
docId,
|
||||
title: doc.title,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ When doing collaborative editing, doc?.title might be out of sync
|
||||
* causing an infinite loop of updates.
|
||||
* To prevent this, we only run this effect when doc?.title changes,
|
||||
* not when inlineContent.props.title changes.
|
||||
*/
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [doc?.title, docId, isEditable]);
|
||||
|
||||
const LinkSelected = ({ url, title }: LinkSelectedProps) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(title);
|
||||
@@ -127,7 +74,7 @@ export const LinkSelected = ({
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
void router.push(`/docs/${docId}/`);
|
||||
void router.push(url);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -247,6 +247,7 @@ export const SearchPage = ({
|
||||
{
|
||||
type: 'interlinkingLinkInline',
|
||||
props: {
|
||||
url: `/docs/${doc.id}`,
|
||||
docId: doc.id,
|
||||
title: doc.title || untitledDocument,
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './BlockNoteEditor';
|
||||
export * from './DocEditor';
|
||||
export * from './EmojiPicker';
|
||||
export * from './custom-blocks/';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { deriveMediaFilename } from '../utils_html';
|
||||
import { deriveMediaFilename } from '../utils';
|
||||
|
||||
describe('deriveMediaFilename', () => {
|
||||
test('uses last URL segment when src is a valid URL', () => {
|
||||
|
||||
@@ -184,75 +184,6 @@ s {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Remove bullet points from checkbox lists */
|
||||
ul.checklist,
|
||||
ul:has(li input[type='checkbox']) {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
ul.checklist li,
|
||||
ul:has(li input[type='checkbox']) li {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
ul.checklist li input[type='checkbox'],
|
||||
ul:has(li input[type='checkbox']) li input[type='checkbox'] {
|
||||
margin: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
ul.checklist li p,
|
||||
ul:has(li input[type='checkbox']) li p {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Native HTML Lists - remove default margins */
|
||||
ol,
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
/* Nested lists */
|
||||
ul ul {
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
/* Keep decimal numbering for nested ol (remove this if you want letters) */
|
||||
ol ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
li p {
|
||||
margin: 0;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Quotes */
|
||||
blockquote,
|
||||
.bn-block-content[data-content-type='quote'] blockquote {
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
/**
|
||||
* 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';
|
||||
@@ -34,15 +30,6 @@ 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';
|
||||
@@ -55,7 +42,7 @@ export const blockMappingHeadingPDF: DocsExporterPDF['mappings']['blockMapping']
|
||||
title: bookmarkTitle,
|
||||
}}
|
||||
style={{
|
||||
fontSize: levelFontSizeEM * FONT_SIZE * PIXELS_PER_POINT,
|
||||
fontSize: fontSizeEM * FONT_SIZE * PIXELS_PER_POINT,
|
||||
fontWeight: 700,
|
||||
marginTop: `${fontSizeEM * MERGE_RATIO}px`,
|
||||
marginBottom: `${fontSizeEM * MERGE_RATIO}px`,
|
||||
|
||||
@@ -21,6 +21,9 @@ 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 };
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Select,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@gouvfr-lasuite/cunningham-react';
|
||||
} from '@openfun/cunningham-react';
|
||||
import { DocumentProps, pdf } from '@react-pdf/renderer';
|
||||
import jsonemoji from 'emoji-datasource-apple' assert { type: 'json' };
|
||||
import i18next from 'i18next';
|
||||
@@ -29,12 +29,11 @@ import { TemplatesOrdering, useTemplates } from '../api/useTemplates';
|
||||
import { docxDocsSchemaMappings } from '../mappingDocx';
|
||||
import { odtDocsSchemaMappings } from '../mappingODT';
|
||||
import { pdfDocsSchemaMappings } from '../mappingPDF';
|
||||
import { downloadFile } from '../utils';
|
||||
import {
|
||||
addMediaFilesToZip,
|
||||
downloadFile,
|
||||
generateHtmlDocument,
|
||||
improveHtmlAccessibility,
|
||||
} from '../utils_html';
|
||||
} from '../utils';
|
||||
|
||||
enum DocDownloadFormat {
|
||||
HTML = 'html',
|
||||
@@ -162,12 +161,10 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
|
||||
const zip = new JSZip();
|
||||
|
||||
improveHtmlAccessibility(parsedDocument, documentTitle);
|
||||
await addMediaFilesToZip(parsedDocument, zip, mediaUrl);
|
||||
|
||||
const lang = i18next.language || fallbackLng;
|
||||
const body = parsedDocument.body;
|
||||
const editorHtmlWithLocalMedia = body ? body.innerHTML : '';
|
||||
const editorHtmlWithLocalMedia = parsedDocument.body.innerHTML;
|
||||
|
||||
const htmlContent = generateHtmlDocument(
|
||||
documentTitle,
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
export * from './api';
|
||||
export * from './utils';
|
||||
export * from './utils_html';
|
||||
|
||||
import * as ModalExport from './components/ModalExport';
|
||||
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
import { ExternalHyperlink, TextRun } from 'docx';
|
||||
|
||||
import { getEmojiAndTitle } from '@/docs/doc-management';
|
||||
|
||||
import { DocsExporterDocx } from '../types';
|
||||
|
||||
export const inlineContentMappingInterlinkingLinkDocx: DocsExporterDocx['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
|
||||
(inline) => {
|
||||
if (!inline.props.docId) {
|
||||
return new TextRun('');
|
||||
}
|
||||
|
||||
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(inline.props.title);
|
||||
|
||||
return new ExternalHyperlink({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `${emoji || '📄'}${titleWithoutEmoji}`,
|
||||
text: `📄${inline.props.title}`,
|
||||
bold: true,
|
||||
}),
|
||||
],
|
||||
link: window.location.origin + `/docs/${inline.props.docId}/`,
|
||||
link: window.location.origin + inline.props.url,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getEmojiAndTitle } from '@/docs/doc-management';
|
||||
|
||||
import { DocsExporterODT } from '../types';
|
||||
|
||||
export const inlineContentMappingInterlinkingLinkODT: DocsExporterODT['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
|
||||
(inline) => {
|
||||
if (!inline.props.docId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(inline.props.title);
|
||||
const url = window.location.origin + `/docs/${inline.props.docId}/`;
|
||||
const url = window.location.origin + inline.props.url;
|
||||
const title = inline.props.title;
|
||||
|
||||
// Create ODT hyperlink using React.createElement to avoid TypeScript JSX namespace issues
|
||||
// Uses the same structure as BlockNote's default link mapping
|
||||
@@ -24,6 +18,6 @@ export const inlineContentMappingInterlinkingLinkODT: DocsExporterODT['mappings'
|
||||
xlinkShow: 'replace',
|
||||
xlinkHref: url,
|
||||
},
|
||||
`${emoji || '📄'}${titleWithoutEmoji}`,
|
||||
`📄${title}`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
import { Image, Link, Text } from '@react-pdf/renderer';
|
||||
|
||||
import { getEmojiAndTitle } from '@/docs/doc-management';
|
||||
|
||||
import DocSelectedIcon from '../assets/doc-selected.png';
|
||||
import { DocsExporterPDF } from '../types';
|
||||
|
||||
export const inlineContentMappingInterlinkingLinkPDF: DocsExporterPDF['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
|
||||
(inline) => {
|
||||
if (!inline.props.docId) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(inline.props.title);
|
||||
|
||||
return (
|
||||
<Link
|
||||
src={window.location.origin + `/docs/${inline.props.docId}/`}
|
||||
src={window.location.origin + inline.props.url}
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
color: 'black',
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
{emoji || <Image src={DocSelectedIcon.src} />}{' '}
|
||||
<Text>{titleWithoutEmoji}</Text>{' '}
|
||||
<Image src={DocSelectedIcon.src} />{' '}
|
||||
<Text>{inline.props.title}</Text>{' '}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,8 +5,11 @@ import {
|
||||
} from '@blocknote/core';
|
||||
import { Canvg } from 'canvg';
|
||||
import { IParagraphOptions, ShadingType } from 'docx';
|
||||
import JSZip from 'jszip';
|
||||
import React from 'react';
|
||||
|
||||
import { exportResolveFileUrl } from './api';
|
||||
|
||||
export function downloadFile(blob: Blob, filename: string) {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
@@ -33,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');
|
||||
@@ -51,36 +54,26 @@ export async function convertSvgToPng(
|
||||
const svgElement = svgDoc.documentElement;
|
||||
|
||||
// Get viewBox or fallback to width/height attributes
|
||||
let calculatedHeight: number | undefined;
|
||||
let height;
|
||||
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];
|
||||
|
||||
const svg = Canvg.fromString(ctx, svgText);
|
||||
|
||||
const FALLBACK_WIDTH = 536;
|
||||
|
||||
// Resize if width provided, preserving aspect ratio
|
||||
if (originalWidth && originalHeight && width) {
|
||||
if (originalWidth && originalHeight) {
|
||||
const aspectRatio = originalHeight / originalWidth;
|
||||
calculatedHeight = Math.round(width * aspectRatio);
|
||||
svg.resize(width, calculatedHeight, true);
|
||||
} else if (!width && !originalWidth) {
|
||||
svg.resize(FALLBACK_WIDTH, undefined, true);
|
||||
height = Math.round(width * aspectRatio);
|
||||
}
|
||||
|
||||
const svg = Canvg.fromString(ctx, svgText);
|
||||
svg.resize(width, height, true);
|
||||
await svg.render();
|
||||
|
||||
const returnWidth = width || originalWidth || FALLBACK_WIDTH;
|
||||
const returnHeight = calculatedHeight || returnWidth;
|
||||
|
||||
return {
|
||||
png: canvas.toDataURL('image/png'),
|
||||
width: returnWidth,
|
||||
height: returnHeight,
|
||||
width,
|
||||
height: height || width,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -189,3 +182,172 @@ export function odtRegisterParagraphStyleForBlock(
|
||||
|
||||
return styleName;
|
||||
}
|
||||
|
||||
// Escape user-provided text before injecting it into the exported HTML document.
|
||||
export const escapeHtml = (value: string): string =>
|
||||
value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
interface MediaFilenameParams {
|
||||
src: string;
|
||||
index: number;
|
||||
blob: Blob;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a stable, readable filename for media exported in the HTML ZIP.
|
||||
*
|
||||
* Rules:
|
||||
* - Default base name is "media-{index+1}".
|
||||
* - For non data: URLs, we reuse the last path segment when possible (e.g. 1-photo.png).
|
||||
* - If the base name has no extension, we try to infer one from the blob MIME type.
|
||||
*/
|
||||
export const deriveMediaFilename = ({
|
||||
src,
|
||||
index,
|
||||
blob,
|
||||
}: MediaFilenameParams): string => {
|
||||
// Default base name
|
||||
let baseName = `media-${index + 1}`;
|
||||
|
||||
// Try to reuse the last path segment for non data URLs.
|
||||
if (!src.startsWith('data:')) {
|
||||
try {
|
||||
const url = new URL(src, window.location.origin);
|
||||
const lastSegment = url.pathname.split('/').pop();
|
||||
if (lastSegment) {
|
||||
baseName = `${index + 1}-${lastSegment}`;
|
||||
}
|
||||
} catch {
|
||||
// Ignore invalid URLs, keep default baseName.
|
||||
}
|
||||
}
|
||||
|
||||
let filename = baseName;
|
||||
|
||||
// Ensure the filename has an extension consistent with the blob MIME type.
|
||||
const mimeType = blob.type;
|
||||
if (mimeType && !baseName.includes('.')) {
|
||||
const slashIndex = mimeType.indexOf('/');
|
||||
const rawSubtype =
|
||||
slashIndex !== -1 && slashIndex < mimeType.length - 1
|
||||
? mimeType.slice(slashIndex + 1)
|
||||
: '';
|
||||
|
||||
let extension = '';
|
||||
const subtype = rawSubtype.toLowerCase();
|
||||
|
||||
if (subtype.includes('svg')) {
|
||||
extension = 'svg';
|
||||
} else if (subtype.includes('jpeg') || subtype.includes('pjpeg')) {
|
||||
extension = 'jpg';
|
||||
} else if (subtype.includes('png')) {
|
||||
extension = 'png';
|
||||
} else if (subtype.includes('gif')) {
|
||||
extension = 'gif';
|
||||
} else if (subtype.includes('webp')) {
|
||||
extension = 'webp';
|
||||
} else if (subtype.includes('pdf')) {
|
||||
extension = 'pdf';
|
||||
} else if (subtype) {
|
||||
extension = subtype.split('+')[0];
|
||||
}
|
||||
|
||||
if (extension) {
|
||||
filename = `${baseName}.${extension}`;
|
||||
}
|
||||
}
|
||||
|
||||
return filename;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a complete HTML document structure for export.
|
||||
*
|
||||
* @param documentTitle - The title of the document (will be escaped)
|
||||
* @param editorHtmlWithLocalMedia - The HTML content from the editor
|
||||
* @param lang - The language code for the document (e.g., 'fr', 'en')
|
||||
* @returns A complete HTML5 document string
|
||||
*/
|
||||
export const generateHtmlDocument = (
|
||||
documentTitle: string,
|
||||
editorHtmlWithLocalMedia: string,
|
||||
lang: string,
|
||||
): string => {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="${lang}">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>${escapeHtml(documentTitle)}</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main role="main">
|
||||
${editorHtmlWithLocalMedia}
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
};
|
||||
|
||||
export const addMediaFilesToZip = async (
|
||||
parsedDocument: Document,
|
||||
zip: JSZip,
|
||||
mediaUrl: string,
|
||||
) => {
|
||||
const mediaFiles: { filename: string; blob: Blob }[] = [];
|
||||
const mediaElements = Array.from(
|
||||
parsedDocument.querySelectorAll<
|
||||
HTMLImageElement | HTMLVideoElement | HTMLAudioElement | HTMLSourceElement
|
||||
>('img, video, audio, source'),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
mediaElements.map(async (element, index) => {
|
||||
const src = element.getAttribute('src');
|
||||
|
||||
if (!src) {
|
||||
return;
|
||||
}
|
||||
|
||||
// data: URLs are already embedded and work offline; no need to create separate files.
|
||||
if (src.startsWith('data:')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only download same-origin resources (internal media like /media/...).
|
||||
// External URLs keep their original src and are not included in the ZIP
|
||||
let url: URL | null = null;
|
||||
try {
|
||||
url = new URL(src, mediaUrl);
|
||||
} catch {
|
||||
url = null;
|
||||
}
|
||||
|
||||
if (!url || url.origin !== mediaUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetched = await exportResolveFileUrl(url.href);
|
||||
|
||||
if (!(fetched instanceof Blob)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = deriveMediaFilename({
|
||||
src: url.href,
|
||||
index,
|
||||
blob: fetched,
|
||||
});
|
||||
element.setAttribute('src', filename);
|
||||
mediaFiles.push({ filename, blob: fetched });
|
||||
}),
|
||||
);
|
||||
|
||||
mediaFiles.forEach(({ filename, blob }) => {
|
||||
zip.file(filename, blob);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,434 +0,0 @@
|
||||
import JSZip from 'jszip';
|
||||
|
||||
import { exportResolveFileUrl } from './api';
|
||||
|
||||
// Escape user-provided text before injecting it into the exported HTML document.
|
||||
export const escapeHtml = (value: string): string =>
|
||||
value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
/**
|
||||
* Derives a stable, readable filename for media exported in the HTML ZIP.
|
||||
*
|
||||
* Rules:
|
||||
* - Default base name is "media-{index+1}".
|
||||
* - For non data: URLs, we reuse the last path segment when possible (e.g. 1-photo.png).
|
||||
* - If the base name has no extension, we try to infer one from the blob MIME type.
|
||||
*/
|
||||
|
||||
interface MediaFilenameParams {
|
||||
src: string;
|
||||
index: number;
|
||||
blob: Blob;
|
||||
}
|
||||
|
||||
export const deriveMediaFilename = ({
|
||||
src,
|
||||
index,
|
||||
blob,
|
||||
}: MediaFilenameParams): string => {
|
||||
// Default base name
|
||||
let baseName = `media-${index + 1}`;
|
||||
|
||||
// Try to reuse the last path segment for non data URLs.
|
||||
if (!src.startsWith('data:')) {
|
||||
try {
|
||||
const url = new URL(src, window.location.origin);
|
||||
const lastSegment = url.pathname.split('/').pop();
|
||||
if (lastSegment) {
|
||||
baseName = `${index + 1}-${lastSegment}`;
|
||||
}
|
||||
} catch {
|
||||
// Ignore invalid URLs, keep default baseName.
|
||||
}
|
||||
}
|
||||
|
||||
let filename = baseName;
|
||||
|
||||
// Ensure the filename has an extension consistent with the blob MIME type.
|
||||
const mimeType = blob.type;
|
||||
if (mimeType && !baseName.includes('.')) {
|
||||
const slashIndex = mimeType.indexOf('/');
|
||||
const rawSubtype =
|
||||
slashIndex !== -1 && slashIndex < mimeType.length - 1
|
||||
? mimeType.slice(slashIndex + 1)
|
||||
: '';
|
||||
|
||||
let extension = '';
|
||||
const subtype = rawSubtype.toLowerCase();
|
||||
|
||||
if (subtype.includes('svg')) {
|
||||
extension = 'svg';
|
||||
} else if (subtype.includes('jpeg') || subtype.includes('pjpeg')) {
|
||||
extension = 'jpg';
|
||||
} else if (subtype.includes('png')) {
|
||||
extension = 'png';
|
||||
} else if (subtype.includes('gif')) {
|
||||
extension = 'gif';
|
||||
} else if (subtype.includes('webp')) {
|
||||
extension = 'webp';
|
||||
} else if (subtype.includes('pdf')) {
|
||||
extension = 'pdf';
|
||||
} else if (subtype) {
|
||||
extension = subtype.split('+')[0];
|
||||
}
|
||||
|
||||
if (extension) {
|
||||
filename = `${baseName}.${extension}`;
|
||||
}
|
||||
}
|
||||
|
||||
return filename;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a complete HTML document structure for export.
|
||||
*
|
||||
* @param documentTitle - The title of the document (will be escaped)
|
||||
* @param editorHtmlWithLocalMedia - The HTML content from the editor
|
||||
* @param lang - The language code for the document (e.g., 'fr', 'en')
|
||||
* @returns A complete HTML5 document string
|
||||
*/
|
||||
export const generateHtmlDocument = (
|
||||
documentTitle: string,
|
||||
editorHtmlWithLocalMedia: string,
|
||||
lang: string,
|
||||
): string => {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="${lang}">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>${escapeHtml(documentTitle)}</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main role="main">
|
||||
${editorHtmlWithLocalMedia}
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Enrich the HTML produced by the editor with semantic tags and basic a11y defaults.
|
||||
*
|
||||
* Notes:
|
||||
* - We work directly on the parsed Document so modifications are reflected before we zip files.
|
||||
* - We keep the editor inner structure but upgrade the key block types to native elements.
|
||||
*/
|
||||
export const improveHtmlAccessibility = (
|
||||
parsedDocument: Document,
|
||||
documentTitle: string,
|
||||
) => {
|
||||
const body = parsedDocument.body;
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) Headings: convert heading blocks to h1-h6 based on data-level
|
||||
const headingBlocks = Array.from(
|
||||
body.querySelectorAll<HTMLElement>("[data-content-type='heading']"),
|
||||
);
|
||||
|
||||
headingBlocks.forEach((block) => {
|
||||
const rawLevel = Number(block.getAttribute('data-level')) || 1;
|
||||
const level = Math.min(Math.max(rawLevel, 1), 6);
|
||||
const heading = parsedDocument.createElement(`h${level}`);
|
||||
heading.innerHTML = block.innerHTML;
|
||||
block.replaceWith(heading);
|
||||
});
|
||||
|
||||
// 2) Lists: convert to semantic OL/UL/LI elements for accessibility
|
||||
const listItemSelector =
|
||||
"[data-content-type='bulletListItem'], [data-content-type='numberedListItem']";
|
||||
|
||||
// Helper function to get nesting level by counting block-group ancestors
|
||||
const getNestingLevel = (blockOuter: HTMLElement): number => {
|
||||
let level = 0;
|
||||
let parent = blockOuter.parentElement;
|
||||
while (parent) {
|
||||
if (parent.classList.contains('bn-block-group')) {
|
||||
level++;
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
return level;
|
||||
};
|
||||
|
||||
// Find all block-outer elements in document order
|
||||
const allBlockOuters = Array.from(
|
||||
body.querySelectorAll<HTMLElement>('.bn-block-outer'),
|
||||
);
|
||||
|
||||
// Collect list items with their info before modifying DOM
|
||||
interface ListItemInfo {
|
||||
blockOuter: HTMLElement;
|
||||
listItem: HTMLElement;
|
||||
contentType: string;
|
||||
level: number;
|
||||
}
|
||||
|
||||
const listItemsInfo: ListItemInfo[] = [];
|
||||
allBlockOuters.forEach((blockOuter) => {
|
||||
const listItem = blockOuter.querySelector<HTMLElement>(listItemSelector);
|
||||
if (listItem) {
|
||||
const contentType = listItem.getAttribute('data-content-type');
|
||||
if (contentType) {
|
||||
const level = getNestingLevel(blockOuter);
|
||||
listItemsInfo.push({
|
||||
blockOuter,
|
||||
listItem,
|
||||
contentType,
|
||||
level,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Stack to track lists at each nesting level
|
||||
const listStack: Array<{ list: HTMLElement; type: string; level: number }> =
|
||||
[];
|
||||
|
||||
listItemsInfo.forEach((info, idx) => {
|
||||
const { blockOuter, listItem, contentType, level } = info;
|
||||
const isBullet = contentType === 'bulletListItem';
|
||||
const listTag = isBullet ? 'ul' : 'ol';
|
||||
|
||||
// Check if previous item continues the same list (same type and level)
|
||||
const previousInfo = idx > 0 ? listItemsInfo[idx - 1] : null;
|
||||
const continuesPreviousList =
|
||||
previousInfo &&
|
||||
previousInfo.contentType === contentType &&
|
||||
previousInfo.level === level;
|
||||
|
||||
// Find or create the appropriate list
|
||||
let targetList: HTMLElement | null = null;
|
||||
|
||||
if (continuesPreviousList) {
|
||||
// Continue with the list at this level from stack
|
||||
const listAtLevel = listStack.find((item) => item.level === level);
|
||||
targetList = listAtLevel?.list || null;
|
||||
}
|
||||
|
||||
// If no list found, create a new one
|
||||
if (!targetList) {
|
||||
targetList = parsedDocument.createElement(listTag);
|
||||
|
||||
// Remove lists from stack that are at same or deeper level
|
||||
while (
|
||||
listStack.length > 0 &&
|
||||
listStack[listStack.length - 1].level >= level
|
||||
) {
|
||||
listStack.pop();
|
||||
}
|
||||
|
||||
// If we have a parent list, nest this list inside its last li
|
||||
if (
|
||||
listStack.length > 0 &&
|
||||
listStack[listStack.length - 1].level < level
|
||||
) {
|
||||
const parentList = listStack[listStack.length - 1].list;
|
||||
const lastLi = parentList.querySelector('li:last-child');
|
||||
if (lastLi) {
|
||||
lastLi.appendChild(targetList);
|
||||
} else {
|
||||
// No li yet, create one and add the nested list
|
||||
const li = parsedDocument.createElement('li');
|
||||
parentList.appendChild(li);
|
||||
li.appendChild(targetList);
|
||||
}
|
||||
} else {
|
||||
// Top-level list
|
||||
blockOuter.parentElement?.insertBefore(targetList, blockOuter);
|
||||
}
|
||||
|
||||
// Add to stack
|
||||
listStack.push({ list: targetList, type: contentType, level });
|
||||
}
|
||||
|
||||
// Create list item and add content
|
||||
const li = parsedDocument.createElement('li');
|
||||
li.innerHTML = listItem.innerHTML;
|
||||
targetList.appendChild(li);
|
||||
|
||||
// Remove original block-outer
|
||||
blockOuter.remove();
|
||||
});
|
||||
|
||||
// 3) Quotes -> <blockquote>
|
||||
const quoteBlocks = Array.from(
|
||||
body.querySelectorAll<HTMLElement>("[data-content-type='quote']"),
|
||||
);
|
||||
quoteBlocks.forEach((block) => {
|
||||
const quote = parsedDocument.createElement('blockquote');
|
||||
quote.innerHTML = block.innerHTML;
|
||||
block.replaceWith(quote);
|
||||
});
|
||||
|
||||
// 4) Callouts -> <aside role="note">
|
||||
const calloutBlocks = Array.from(
|
||||
body.querySelectorAll<HTMLElement>("[data-content-type='callout']"),
|
||||
);
|
||||
calloutBlocks.forEach((block) => {
|
||||
const aside = parsedDocument.createElement('aside');
|
||||
aside.setAttribute('role', 'note');
|
||||
aside.innerHTML = block.innerHTML;
|
||||
block.replaceWith(aside);
|
||||
});
|
||||
|
||||
// 5) Checklists -> list + checkbox semantics
|
||||
const checkListItems = Array.from(
|
||||
body.querySelectorAll<HTMLElement>("[data-content-type='checkListItem']"),
|
||||
);
|
||||
checkListItems.forEach((item) => {
|
||||
const parent = item.parentElement;
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
let previousSibling = item.previousElementSibling;
|
||||
let listContainer: HTMLElement | null = null;
|
||||
|
||||
if (previousSibling?.tagName.toLowerCase() === 'ul') {
|
||||
listContainer = previousSibling as HTMLElement;
|
||||
} else {
|
||||
listContainer = parsedDocument.createElement('ul');
|
||||
listContainer.setAttribute('role', 'list');
|
||||
listContainer.classList.add('checklist');
|
||||
parent.insertBefore(listContainer, item);
|
||||
}
|
||||
|
||||
const li = parsedDocument.createElement('li');
|
||||
li.innerHTML = item.innerHTML;
|
||||
|
||||
// Ensure checkbox has an accessible state; fall back to aria-checked if missing.
|
||||
const checkbox = li.querySelector<HTMLInputElement>(
|
||||
"input[type='checkbox']",
|
||||
);
|
||||
if (checkbox && !checkbox.hasAttribute('aria-checked')) {
|
||||
checkbox.setAttribute(
|
||||
'aria-checked',
|
||||
checkbox.checked ? 'true' : 'false',
|
||||
);
|
||||
}
|
||||
|
||||
listContainer.appendChild(li);
|
||||
parent.removeChild(item);
|
||||
});
|
||||
|
||||
// 6) Code blocks -> <pre><code>
|
||||
const codeBlocks = Array.from(
|
||||
body.querySelectorAll<HTMLElement>("[data-content-type='codeBlock']"),
|
||||
);
|
||||
codeBlocks.forEach((block) => {
|
||||
const pre = parsedDocument.createElement('pre');
|
||||
const code = parsedDocument.createElement('code');
|
||||
|
||||
// Preserve existing classes/attributes so the exported CSS (dark theme) still applies.
|
||||
pre.className = block.className || '';
|
||||
pre.setAttribute('data-content-type', 'codeBlock');
|
||||
|
||||
// Copy other data attributes from the original block to the new <pre>.
|
||||
Array.from(block.attributes).forEach((attr) => {
|
||||
if (attr.name.startsWith('data-') && attr.name !== 'data-content-type') {
|
||||
pre.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
});
|
||||
|
||||
// Move content inside <code>.
|
||||
code.innerHTML = block.innerHTML;
|
||||
pre.appendChild(code);
|
||||
block.replaceWith(pre);
|
||||
});
|
||||
|
||||
// 7) Ensure images have alt text (empty when not provided)
|
||||
body.querySelectorAll<HTMLImageElement>('img').forEach((img) => {
|
||||
if (!img.hasAttribute('alt')) {
|
||||
img.setAttribute('alt', '');
|
||||
}
|
||||
});
|
||||
|
||||
// 8) Wrap content in an article with a title landmark if none exists
|
||||
const existingH1 = body.querySelector('h1');
|
||||
if (!existingH1) {
|
||||
const titleHeading = parsedDocument.createElement('h1');
|
||||
titleHeading.id = 'doc-title';
|
||||
titleHeading.textContent = documentTitle;
|
||||
body.insertBefore(titleHeading, body.firstChild);
|
||||
}
|
||||
|
||||
// If there is no article, group the body content inside one for better semantics.
|
||||
const hasArticle = body.querySelector('article');
|
||||
if (!hasArticle) {
|
||||
const article = parsedDocument.createElement('article');
|
||||
article.setAttribute('role', 'document');
|
||||
article.setAttribute('aria-labelledby', 'doc-title');
|
||||
while (body.firstChild) {
|
||||
article.appendChild(body.firstChild);
|
||||
}
|
||||
body.appendChild(article);
|
||||
}
|
||||
};
|
||||
|
||||
export const addMediaFilesToZip = async (
|
||||
parsedDocument: Document,
|
||||
zip: JSZip,
|
||||
mediaUrl: string,
|
||||
) => {
|
||||
const mediaFiles: { filename: string; blob: Blob }[] = [];
|
||||
const mediaElements = Array.from(
|
||||
parsedDocument.querySelectorAll<
|
||||
HTMLImageElement | HTMLVideoElement | HTMLAudioElement | HTMLSourceElement
|
||||
>('img, video, audio, source'),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
mediaElements.map(async (element, index) => {
|
||||
const src = element.getAttribute('src');
|
||||
|
||||
if (!src) {
|
||||
return;
|
||||
}
|
||||
|
||||
// data: URLs are already embedded and work offline; no need to create separate files.
|
||||
if (src.startsWith('data:')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only download same-origin resources (internal media like /media/...).
|
||||
// External URLs keep their original src and are not included in the ZIP
|
||||
let url: URL | null = null;
|
||||
try {
|
||||
url = new URL(src, mediaUrl);
|
||||
} catch {
|
||||
url = null;
|
||||
}
|
||||
|
||||
if (!url || url.origin !== mediaUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetched = await exportResolveFileUrl(url.href);
|
||||
|
||||
if (!(fetched instanceof Blob)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = deriveMediaFilename({
|
||||
src: url.href,
|
||||
index,
|
||||
blob: fetched,
|
||||
});
|
||||
element.setAttribute('src', filename);
|
||||
mediaFiles.push({ filename, blob: fetched });
|
||||
}),
|
||||
);
|
||||
|
||||
mediaFiles.forEach(({ filename, blob }) => {
|
||||
zip.file(filename, blob);
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Button, Modal, ModalSize } from '@openfun/cunningham-react';
|
||||
import { t } from 'i18next';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
|
||||
import {
|
||||
Button,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Card, Icon } from '@/components';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tooltip } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Tooltip } from '@openfun/cunningham-react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button, useModal } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
|
||||
import { Button, useModal } from '@openfun/cunningham-react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@gouvfr-lasuite/cunningham-react';
|
||||
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useEditorStore } from '../../doc-editor';
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@gouvfr-lasuite/cunningham-react';
|
||||
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
||||
import {
|
||||
UseMutationOptions,
|
||||
useMutation,
|
||||
|
||||
@@ -1,27 +1,10 @@
|
||||
import { MouseEvent, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import {
|
||||
Box,
|
||||
BoxButton,
|
||||
BoxButtonType,
|
||||
EmojiPicker,
|
||||
PICKER_HEIGHT,
|
||||
Text,
|
||||
TextType,
|
||||
emojidata,
|
||||
} from '@/components';
|
||||
import { BoxButton, BoxButtonType, Text, TextType } from '@/components';
|
||||
import { EmojiPicker, emojidata } from '@/docs/doc-editor/';
|
||||
|
||||
import { useDocTitleUpdate } from '../hooks/useDocTitleUpdate';
|
||||
import { cssSelectors } from '../utils';
|
||||
|
||||
const getClosestTreeItem = (element: HTMLElement | null) =>
|
||||
element?.closest<HTMLElement>(cssSelectors.DOC_TREE_ROW) ??
|
||||
element?.closest<HTMLElement>(cssSelectors.DOC_TREE_NODE) ??
|
||||
element?.closest<HTMLElement>('[role="treeitem"]') ??
|
||||
null;
|
||||
|
||||
type DocIconProps = TextType & {
|
||||
buttonProps?: BoxButtonType;
|
||||
@@ -47,7 +30,6 @@ export const DocIcon = ({
|
||||
...textProps
|
||||
}: DocIconProps) => {
|
||||
const { updateDocEmoji } = useDocTitleUpdate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const iconRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -61,14 +43,6 @@ export const DocIcon = ({
|
||||
return defaultIcon;
|
||||
}
|
||||
|
||||
const emojiLabel = withEmojiPicker
|
||||
? emoji
|
||||
? t('Edit document emoji')
|
||||
: t('Add emoji')
|
||||
: emoji
|
||||
? t('Document emoji')
|
||||
: undefined;
|
||||
|
||||
const toggleEmojiPicker = (e: MouseEvent) => {
|
||||
if (withEmojiPicker) {
|
||||
e.stopPropagation();
|
||||
@@ -76,24 +50,9 @@ export const DocIcon = ({
|
||||
|
||||
if (!openEmojiPicker && iconRef.current) {
|
||||
const rect = iconRef.current.getBoundingClientRect();
|
||||
|
||||
const pickerHeight = PICKER_HEIGHT;
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
|
||||
// Position picker above if not enough space below and enough space above
|
||||
const shouldPositionAbove =
|
||||
spaceBelow < pickerHeight && spaceAbove >= pickerHeight;
|
||||
|
||||
// Offset to align the picker properly
|
||||
const ROW_OFFSET_TOP = 55;
|
||||
const ROW_OFFSET_BOTTOM = 10;
|
||||
|
||||
setPickerPosition({
|
||||
top: shouldPositionAbove
|
||||
? rect.top - pickerHeight + ROW_OFFSET_TOP
|
||||
: rect.bottom + ROW_OFFSET_BOTTOM,
|
||||
left: rect.left,
|
||||
top: rect.bottom + window.scrollY + 8,
|
||||
left: rect.left + window.scrollX,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,30 +76,6 @@ export const DocIcon = ({
|
||||
setOpenEmojiPicker(false);
|
||||
};
|
||||
|
||||
const handleEscape = () => {
|
||||
setOpenEmojiPicker(false);
|
||||
window.requestAnimationFrame(() => {
|
||||
const localTreeItem = getClosestTreeItem(iconRef.current);
|
||||
const docTree = document.querySelector<HTMLElement>(
|
||||
cssSelectors.DOC_TREE,
|
||||
);
|
||||
const docTreeItem =
|
||||
localTreeItem ||
|
||||
docTree?.querySelector<HTMLElement>(
|
||||
cssSelectors.DOC_TREE_FOCUSED_NODE,
|
||||
) ||
|
||||
docTree?.querySelector<HTMLElement>(
|
||||
cssSelectors.DOC_TREE_SELECTED_ROW,
|
||||
) ||
|
||||
docTree?.querySelector<HTMLElement>(
|
||||
cssSelectors.DOC_TREE_SELECTED_NODE,
|
||||
) ||
|
||||
document.querySelector<HTMLElement>(cssSelectors.DOC_TREE_ROOT);
|
||||
|
||||
docTreeItem?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<BoxButton
|
||||
@@ -148,8 +83,6 @@ export const DocIcon = ({
|
||||
ref={iconRef}
|
||||
onClick={toggleEmojiPicker}
|
||||
color="tertiary-text"
|
||||
aria-label={emojiLabel}
|
||||
title={emojiLabel}
|
||||
{...buttonProps}
|
||||
>
|
||||
{!emoji ? (
|
||||
@@ -169,22 +102,21 @@ export const DocIcon = ({
|
||||
</BoxButton>
|
||||
{openEmojiPicker &&
|
||||
createPortal(
|
||||
<Box
|
||||
$position="fixed"
|
||||
$css={css`
|
||||
top: ${pickerPosition.top}px;
|
||||
left: ${pickerPosition.left}px;
|
||||
z-index: 1000;
|
||||
`}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: pickerPosition.top,
|
||||
left: pickerPosition.left,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<EmojiPicker
|
||||
emojiData={emojidata}
|
||||
onEmojiSelect={handleEmojiSelect}
|
||||
onClickOutside={handleClickOutside}
|
||||
withOverlay={true}
|
||||
onEscape={handleEscape}
|
||||
/>
|
||||
</Box>,
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ModalSize,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@gouvfr-lasuite/cunningham-react';
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export * from './useCollaboration';
|
||||
export * from './useCopyDocLink';
|
||||
export * from './useCreateChildDocTree';
|
||||
export * from './useDocFocusManagement';
|
||||
export * from './useDocTitleUpdate';
|
||||
export * from './useDocUtils';
|
||||
export * from './useIsCollaborativeEditable';
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { cssSelectors } from '@/docs/doc-management/utils';
|
||||
|
||||
const isWithin = (el: Element | null, selector: string) =>
|
||||
!!el?.closest(selector);
|
||||
|
||||
export const useDocFocusManagement = (docId?: string, isReady = true) => {
|
||||
// 1) Auto-focus title when opening a doc
|
||||
useEffect(() => {
|
||||
if (!docId || !isReady || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const frameId = window.requestAnimationFrame(() => {
|
||||
const titleElement = document.querySelector<HTMLElement>(
|
||||
cssSelectors.DOC_TITLE,
|
||||
);
|
||||
if (!titleElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid stealing focus if user is already in the doc tree or editor.
|
||||
const activeEl = document.activeElement;
|
||||
const active = activeEl instanceof Element ? activeEl : null;
|
||||
const isInDocUI =
|
||||
isWithin(active, cssSelectors.DOC_EDITOR_FOCUS) ||
|
||||
isWithin(active, cssSelectors.DOC_TREE);
|
||||
|
||||
const isBodyFocused = activeEl === document.body;
|
||||
|
||||
if (isBodyFocused && !isInDocUI && activeEl !== titleElement) {
|
||||
titleElement.focus();
|
||||
}
|
||||
});
|
||||
|
||||
return () => window.cancelAnimationFrame(frameId);
|
||||
}, [docId, isReady]);
|
||||
|
||||
// 2) Escape from editor/title -> focus back the selected tree item (or root)
|
||||
useEffect(() => {
|
||||
if (!docId || !isReady || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleFocusShortcut = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'F6' || event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
const activeEl = document.activeElement;
|
||||
const active = activeEl instanceof Element ? activeEl : null;
|
||||
|
||||
const isDocFocus =
|
||||
isWithin(target, cssSelectors.DOC_EDITOR_FOCUS) ||
|
||||
isWithin(active, cssSelectors.DOC_EDITOR_FOCUS) ||
|
||||
isWithin(target, cssSelectors.DOC_TITLE) ||
|
||||
isWithin(active, cssSelectors.DOC_TITLE);
|
||||
|
||||
if (!isDocFocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
const docTree = document.querySelector<HTMLElement>(
|
||||
cssSelectors.DOC_TREE,
|
||||
);
|
||||
|
||||
const docTreeItem =
|
||||
docTree?.querySelector<HTMLElement>(
|
||||
cssSelectors.DOC_TREE_SELECTED_ROW,
|
||||
) ||
|
||||
docTree?.querySelector<HTMLElement>(
|
||||
cssSelectors.DOC_TREE_SELECTED_NODE,
|
||||
) ||
|
||||
document.querySelector<HTMLElement>(cssSelectors.DOC_TREE_ROOT);
|
||||
|
||||
if (!docTreeItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
docTreeItem.focus();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleFocusShortcut, true);
|
||||
return () =>
|
||||
document.removeEventListener('keydown', handleFocusShortcut, true);
|
||||
}, [docId, isReady]);
|
||||
};
|
||||
@@ -38,15 +38,3 @@ export const getEmojiAndTitle = (title: string) => {
|
||||
|
||||
return { emoji: null, titleWithoutEmoji: title };
|
||||
};
|
||||
|
||||
export const cssSelectors = {
|
||||
DOC_TITLE: '.--docs--doc-title-input[contenteditable="true"]',
|
||||
DOC_TREE_ROOT: '[data-testid="doc-tree-root-item"]',
|
||||
DOC_TREE: '[data-testid="doc-tree"]',
|
||||
DOC_EDITOR_FOCUS: '.--docs--main-editor, .--docs--doc-title-input',
|
||||
DOC_TREE_ROW: '.c__tree-view--row',
|
||||
DOC_TREE_NODE: '.c__tree-view--node',
|
||||
DOC_TREE_FOCUSED_NODE: '.c__tree-view--node.isFocused',
|
||||
DOC_TREE_SELECTED_ROW: '.c__tree-view--row[aria-selected="true"]',
|
||||
DOC_TREE_SELECTED_NODE: '.c__tree-view--node[aria-selected="true"]',
|
||||
} as const;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box } from '@/components';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Modal, ModalSize } from '@openfun/cunningham-react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user