Compare commits

..

39 Commits

Author SHA1 Message Date
Sylvain Zimmer
1b200ef7e3 Merge remote-tracking branch 'origin/buildpack' into anct 2025-08-08 10:47:00 +02:00
Sylvain Zimmer
81076f18fe Merge branch 'api_content' into anct 2025-08-08 10:44:51 +02:00
Anthony LC
0cf8b9da1a 🐛(minio) fix user permission error with Minio and Windows
With Minio Docker and Windows, the user ID needs
 to be set to `0:0` to avoid permission issues.
 This change ensures that the Minio container
 runs with root privileges on Windows, which
 is necessary for proper file access and management.
2025-08-07 12:37:00 +02:00
Anthony LC
7be761ce84 🐛(makefile) Windows compatibility fix for Docker volume mounting
On Windows systems, Docker volume paths starting
with a single / can be interpreted incorrectly
by the Docker daemon. The double slash (//) helps
Docker on Windows properly interpret the path as
an absolute path within the container, ensuring
that the working directory is correctly set
when running mail-related yarn commands.
2025-08-07 12:36:29 +02:00
Cyril
5181bba083 ️(a11y) improve keyboard access for language menu and action buttons
Enhances nav for language switch and makes DocsGridActions buttons accessible

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-08-07 11:58:47 +02:00
Anthony LC
f434d78b5d ⬆️(dependencies) update js dependencies
- Update js dependencies
- Fix linters
2025-08-07 11:06:34 +02:00
Cyril
e07f709dd4 (frontend) improve accessibility of global docs home link at top
moved aria-label and added aria-hidden for better accessibility

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-08-06 15:37:51 +02:00
Cyril G
afbacb0a24 ️(frontend) improve left panel accessibility (#1262)
Improve overall accessibility of the left panel:
- ️(frontend) make LeftPanelTargetFilter accessible and use Box as nav
- ️(frontend) improve accessibility in left panel components
- (e2e) fix e2e test to expect aria-current instead of aria-selected
- (frontend) add semantic ul/li to LeftPanel
- (frontend) improve favorite item a11y and update e2e test accordingly
2025-08-06 14:20:53 +02:00
Anthony LC
409e073192 🤡(e2e) mock PATCH language switch
We add some flaky tests because the aria label
selectors were not everytime in english language.
It was because the language switch was not mocked
in the e2e tests, impacting the consistency of
other concurrent tests.
We mock the language switch in the e2e tests
to ensure that the other tests are not impacted
by the language switch.
2025-08-05 12:42:13 +02:00
Jan Conen
886dcb75d5 📝(self-hosted) commands copy-pastable
Make bash commands copy-pastable by prepending
the foldername to the commands.

Signed-off-by: Jan Conen <janconen@hotmail.com>
2025-08-05 11:47:44 +02:00
Jan Conen
bb4d2a9fea 📝(self-hosted) default.conf.template when using nginx-proxy
Add step to copy default.conf.template
hen using nginx-proxy.

Signed-off-by: Jan Conen <janconen@hotmail.com>
2025-08-05 11:47:08 +02:00
Moritz Schlarb
5e5054282e 📝(doc) Fix commands in docker compose section
Corrected the commands in step 1

Signed-off-by: Moritz Schlarb <schlarbm@uni-mainz.de>
2025-08-05 11:14:40 +02:00
timo
f497e75426 🔧(project) Add trailing slash to yprovider api path
The value in the production environment .env example was missing a
trailing slash in the path. This commit adjusts this to be in the same
format as in other places.
2025-08-05 10:02:31 +02:00
Cyril
97ab13ded6 (e2e) fix broken e2e tests by updating selectors
selectors were updated to stabilize and fix the failing e2e tests

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-08-04 16:00:17 +02:00
Cyril
99d674c615 ️(frontend) add correct attributes to decorative and interactive icons
Add aria-hidden and aria-label to improve screen reader accessibility

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-08-04 13:35:48 +02:00
Cyril
1cdb6b62c8 (e2e) ensure i18n.language is injected into generated PDF
Adds an end-to-end test to verify language injection in the generated PDF.

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-08-04 09:35:13 +02:00
Cyril
2bf53301d2 ️(frontend) inject language attribute to pdf export
added language="fr-FR" to <Document /> in ModalExport.tsx via cloneElement()
to improve accessibility and ensure correct screen reader pronunciation

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-08-04 09:35:13 +02:00
Cyril
ec84f31bc7 ️(frontend) set html lang attribute dynamically based on current loc
ensures proper language tag is set for accessibility and SEO compliance

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-08-04 08:42:56 +02:00
rouja
7813219b86 ♻️(documentation) remove unused environment variables
Yesterday during a deployment, we discovered that these variables are
unused:
POSTGRES_DB
POSTGRES_USER
POSTGRES_PASSWORD
2025-08-01 12:42:02 +00:00
Anthony LC
cecb4f5756 🔖(minor) release 3.5.0
Added:
- (helm) Service Account support for K8s Resources in Helm Charts
- (backend) allow masking documents from the list view
- (frontend) subdocs can manage link reach
- (frontend) add duplicate action to doc tree
- (frontend) Interlinking doc
- (frontend) add multi columns support for editor

Changed:
- ♻️(frontend) search on all docs if no children
- ♻️(frontend) redirect to doc after duplicate
- 🔧(project) change env.d system by using local files
- ️(frontend) improve tree stability
- ️(frontend) improve accessibility
- 🛂(frontend) block drag n drop when not desktop

Fixed:
- 🐛(service-worker) Fix useOffline Maximum update depth exceeded
- 🐛(frontend) fix empty left panel after deleting root doc
- 🐛(helm) charts generate invalid YAML for collaboration API / WS
- 🐛(frontend) 401 redirection overridden
- 🐛(frontend) include root parent in search
2025-08-01 09:45:02 +02:00
Anthony LC
63efe40a7b 🐛(frontend) fix interlinking click with Firefox
Fixed Firefox requiring double-click on
interlinks by adding draggable="false" to prevent
drag detection conflicts in contenteditable areas.
2025-08-01 09:45:02 +02:00
AntoLC
e26c3dff35 🌐(i18n) update translated strings
Update translated files with new translations
2025-07-31 14:54:06 +02:00
Anthony LC
f5f9d8a877 (frontend) interlinking export
Create interlinking link mapping for docx and pdf export.
2025-07-31 13:26:09 +02:00
Anthony LC
e7709badbb (frontend) create editor shortcuts hook
We created the editor shortcuts hook to handle
the shortcuts for the editor.
We implemented the following shortcuts:
- "@" to open the interlinking inline content
2025-07-31 13:26:09 +02:00
Anthony LC
2a7c0ef800 (frontend) create page from dropdown search
We are now able to create a new page from
the dropdown search.
2025-07-31 13:26:09 +02:00
Anthony LC
155e7dfe22 (frontend) interlinking custom inline content
We want to be able to interlink documents in the editor.
We created a custom inline content that allows
users to interlink documents.
2025-07-31 13:00:11 +02:00
Anthony LC
afa48b6675 (frontend) create page from slash menu
We are now able to create a new page from
the slash menu.
2025-07-31 12:57:25 +02:00
Anthony LC
f12d30cffa 🚚(frontend) reduce features coupling
Move some components and assets to `doc-management`
to reduce coupling between features:
- SimpleDocItem from `doc-grid` to `doc-management`
- useCreateChildDoc from `doc-tree` to `doc-management`
- isOwnerOrAdmin from `doc-tree` to `doc-management`
2025-07-30 15:11:37 +02:00
Anthony LC
30dfea744a 🐛(frontend) include root parent in search
When searching for documents, the root parent
document is now included in the search
results if it matches the search query.
2025-07-30 14:56:30 +02:00
Anthony LC
2cbe363a5f 🛂(frontend) block drag n drop when not desktop
Scrolling on mobile devices was causing issues
with drag and drop functionality, documents were
being moved unintentionally.
This commit disables drag and drop on mobile devices
to prevent this issue.
2025-07-30 14:06:39 +02:00
Anthony LC
7f450e8aa8 ⬆️(frontend) Bump linkifyjs from 4.3.1 to 4.3.2
Bumps linkifyjs from 4.3.1 to 4.3.2.

---
updated-dependencies:
- dependency-name: linkifyjs
  dependency-version: 4.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 13:21:02 +02:00
Cyril
7021c0f849 (changelog) add accessibility note to CHANGELOG (#1232)
updating Changelog.md with accessibility improvement
2025-07-28 18:07:44 +02:00
Cyril
e8d18d85e9 ️(frontend) improve contrast for links
Updated anchor link color from greyscale-500 to greyscale-600
2025-07-28 17:55:02 +02:00
Sylvain Zimmer
b7ffc766d8 🗑️(convert) cleanup old content route 2025-07-24 14:52:16 +02:00
Sylvain Zimmer
148890a295 (convert) improve tests with stricter tests and less ipsum 2025-07-24 14:49:20 +02:00
Sylvain Zimmer
ab05fa6557 ♻️(convert) reuse existing convert yprovider endpoint for content API 2025-07-24 14:08:18 +02:00
Sylvain Zimmer
cdc24114b6 🚨(lint) fix lint
Oops
2025-07-24 02:42:30 +02:00
Sylvain Zimmer
0bd53aed2c (api) add API route to fetch document content
This allows API users to process document content, enabling the
use of Docs as a headless CMS for instance, or any kind of document
processing. Fixes #1206.
2025-07-24 02:31:50 +02:00
Sylvain Zimmer
ddac6197e3 (buildpack) add PaaS deployment support, tested with Scalingo 2025-06-01 23:54:35 +02:00
154 changed files with 5061 additions and 5000 deletions

View File

@@ -8,14 +8,33 @@ and this project adheres to
## [Unreleased]
### Changed
- ⚡️(frontend) improve accessibility:
- #1248
- #1235
- #1255
- #1262
- #1244
- #1270
### Fixed
- 🐛(makefile) Windows compatibility fix for Docker volume mounting #1264
- 🐛(minio) fix user permission error with Minio and Windows #1264
## [3.5.0] - 2025-07-31
### Added
- ✨(helm) Service Account support for K8s Resources in Helm Charts #778
- ✨(backend) allow masking documents from the list view #1171
- ✨(helm) Service Account support for K8s Resources in Helm Charts #780
- ✨(backend) allow masking documents from the list view #1172
- ✨(frontend) subdocs can manage link reach #1190
- ✨(frontend) add duplicate action to doc tree #1175
- ✨(frontend) Interlinking doc #904
- ✨(frontend) add multi columns support for editor #1219
- ✨(frontend) Can mask a document from the list view #1233
- ✨(api) add API route to fetch document content #1206
### Changed
@@ -23,6 +42,8 @@ and this project adheres to
- ♻️(frontend) redirect to doc after duplicate #1175
- 🔧(project) change env.d system by using local files #1200
- ⚡️(frontend) improve tree stability #1207
- ⚡️(frontend) improve accessibility #1232
- 🛂(frontend) block drag n drop when not desktop #1239
### Fixed
@@ -30,6 +51,7 @@ and this project adheres to
- 🐛(frontend) fix empty left panel after deleting root doc #1197
- 🐛(helm) charts generate invalid YAML for collaboration API / WS #890
- 🐛(frontend) 401 redirection overridden #1214
- 🐛(frontend) include root parent in search #1243
## [3.4.2] - 2025-07-18
@@ -46,7 +68,7 @@ and this project adheres to
### Fixed
- 🌐(frontend) keep simple tag during export #1154
- 🐛(back) manage can-edit endpoint without created room
- 🐛(back) manage can-edit endpoint without created room
in the ws #1152
- 🐛(frontend) fix action buttons not clickable #1162
- 🐛(frontend) fix crash share modal on grid options #1174
@@ -66,11 +88,11 @@ and this project adheres to
- 📝(project) add troubleshoot doc #1066
- 📝(project) add system-requirement doc #1066
- 🔧(frontend) configure x-frame-options to DENY in nginx conf #1084
- ✨(backend) allow to disable checking unsafe mimetype on
- ✨(backend) allow to disable checking unsafe mimetype on
attachment upload #1099
- ✨(doc) add documentation to install with compose #855
- ✨ Give priority to users connected to collaboration server
(aka no websocket feature) #1093
- ✨ Give priority to users connected to collaboration server
(aka no websocket feature) #1093
### Changed
@@ -96,7 +118,6 @@ and this project adheres to
- 🔥(frontend) remove Beta from logo #1095
## [3.3.0] - 2025-05-06
### Added
@@ -122,13 +143,13 @@ and this project adheres to
- ⬆️(docker) upgrade node images to alpine 3.21 #973
### Fixed
- 🐛(y-provider) increase JSON size limits for transcription conversion #989
### Removed
- 🔥(back) remove footer endpoint #948
## [3.2.1] - 2025-05-06
## Fixed
@@ -136,7 +157,6 @@ and this project adheres to
- 🐛(frontend) fix list copy paste #943
- 📝(doc) update contributing policy (commit signatures are now mandatory) #895
## [3.2.0] - 2025-05-05
## Added
@@ -147,7 +167,7 @@ and this project adheres to
- ✨(settings) Allow configuring PKCE for the SSO #886
- 🌐(i18n) activate chinese and spanish languages #884
- 🔧(backend) allow overwriting the data directory #893
- (backend) add `django-lasuite` dependency #839
- (backend) add `django-lasuite` dependency #839
- ✨(frontend) advanced table features #908
## Changed
@@ -198,7 +218,6 @@ and this project adheres to
- 🐛(backend) compute ancestor_links in get_abilities if needed #725
- 🔒️(back) restrict access to document accesses #801
## [2.6.0] - 2025-03-21
## Added
@@ -217,7 +236,6 @@ and this project adheres to
- 🔒️(back) throttle user list endpoint #636
- 🔒️(back) remove pagination and limit to 5 for user list endpoint #636
## [2.5.0] - 2025-03-18
## Added
@@ -240,15 +258,14 @@ and this project adheres to
## Fixed
- 🐛(frontend) SVG export #706
- 🐛(frontend) remove scroll listener table content #688
- 🐛(frontend) remove scroll listener table content #688
- 🔒️(back) restrict access to favorite_list endpoint #690
- 🐛(backend) refactor to fix filtering on children
and descendants views #695
- 🐛(backend) refactor to fix filtering on children
and descendants views #695
- 🐛(action) fix notify-argocd workflow #713
- 🚨(helm) fix helmfile lint #736
- 🚚(frontend) redirect to 401 page when 401 error #759
## [2.4.0] - 2025-03-06
## Added
@@ -263,7 +280,6 @@ and this project adheres to
- 🐛(frontend) fix collaboration error #684
## [2.3.0] - 2025-03-03
## Added
@@ -679,7 +695,8 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.4.2...main
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.5.0...main
[v3.5.0]: https://github.com/suitenumerique/docs/releases/v3.5.0
[v3.4.2]: https://github.com/suitenumerique/docs/releases/v3.4.2
[v3.4.1]: https://github.com/suitenumerique/docs/releases/v3.4.1
[v3.4.0]: https://github.com/suitenumerique/docs/releases/v3.4.0

View File

@@ -35,9 +35,13 @@ DB_PORT = 5432
# -- Docker
# Get the current user ID to use for docker run and docker exec commands
DOCKER_UID = $(shell id -u)
DOCKER_GID = $(shell id -g)
DOCKER_USER = $(DOCKER_UID):$(DOCKER_GID)
ifeq ($(OS),Windows_NT)
DOCKER_USER := 0:0 # run containers as root on Windows
else
DOCKER_UID := $(shell id -u)
DOCKER_GID := $(shell id -g)
DOCKER_USER := $(DOCKER_UID):$(DOCKER_GID)
endif
COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose
COMPOSE_E2E = DOCKER_USER=$(DOCKER_USER) docker compose -f compose.yml -f compose-e2e.yml
COMPOSE_EXEC = $(COMPOSE) exec
@@ -48,7 +52,7 @@ COMPOSE_RUN_CROWDIN = $(COMPOSE_RUN) crowdin crowdin
# -- Backend
MANAGE = $(COMPOSE_RUN_APP) python manage.py
MAIL_YARN = $(COMPOSE_RUN) -w /app/src/mail node yarn
MAIL_YARN = $(COMPOSE_RUN) -w //app/src/mail node yarn
# -- Frontend
PATH_FRONT = ./src/frontend

2
Procfile Normal file
View File

@@ -0,0 +1,2 @@
web: bin/buildpack_start.sh
postdeploy: python manage.py migrate

View File

@@ -38,6 +38,10 @@ function _set_user() {
# options: docker compose command options
# ARGS : docker compose command arguments
function _docker_compose() {
# Set DOCKER_USER for Windows compatibility with MinIO
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || -n "${WSL_DISTRO_NAME:-}" ]]; then
export DOCKER_USER="0:0"
fi
echo "🐳(compose) file: '${COMPOSE_FILE}'"
docker compose \

15
bin/buildpack_postcompile.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
set -o errexit # always exit on error
set -o pipefail # don't ignore exit codes when piping output
echo "-----> Running post-compile script"
rm -rf docker docs env.d gitlint src/frontend/apps/e2e
rm -rf src/frontend/apps
rm -rf src/frontend/packages
# Remove some of the larger packages required by the frontend only
rm -rf src/frontend/node_modules/@next src/frontend/node_modules/next src/frontend/node_modules/react-icons src/frontend/node_modules/@gouvfr-lasuite
# du -ch | sort -rh | head -n 100

15
bin/buildpack_postfrontend.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
set -o errexit # always exit on error
set -o pipefail # don't ignore exit codes when piping output
echo "-----> Running post-frontend script"
# Move the frontend build to the nginx root and clean up
mkdir -p build/
mv src/frontend/apps/impress/out build/frontend-out
mv src/backend/* ./
mv src/nginx/* ./
echo "3.13" > .python-version

18
bin/buildpack_start.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Start the Django backend server
gunicorn -b :8000 impress.wsgi:application --log-file - &
# Start the Y provider service
cd src/frontend/servers/y-provider && PORT=4444 ../../.scalingo/node/bin/node dist/start-server.js &
# Start the Nginx server
bin/run &
# if the current shell is killed, also terminate all its children
trap "pkill SIGTERM -P $$" SIGTERM
# wait for a single child to finish,
wait -n
# then kill all the other tasks
pkill -P $$

View File

@@ -9,9 +9,9 @@
```bash
mkdir keycloak
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/keycloak/compose.yaml
curl -o env.d/kc_postgresql https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/kc_postgresql
curl -o env.d/keycloak https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/keycloak
curl -o keycloak/compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/keycloak/compose.yaml
curl -o keycloak/env.d/kc_postgresql https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/kc_postgresql
curl -o keycloak/env.d/keycloak https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/keycloak
```
### Step 2:. Update `env.d/` files

View File

@@ -9,7 +9,7 @@
```bash
mkdir minio
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/minio/compose.yaml
curl -o minio/compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/minio/compose.yaml
```
### Step 2:. Update compose file with your own values

View File

@@ -13,7 +13,7 @@ Acme-companion is a lightweight companion container for nginx-proxy. It handles
```bash
mkdir nginx-proxy
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/nginx-proxy/compose.yaml
curl -o nginx-proxy/compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/nginx-proxy/compose.yaml
```
### Step 2: Edit `DEFAULT_EMAIL` in the compose file.

View File

@@ -46,9 +46,6 @@ backend:
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: impress
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
REDIS_URL: redis://default:pass@redis-master:6379/1
AWS_S3_ENDPOINT_URL: http://minio.impress.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: root

View File

@@ -31,11 +31,17 @@ For older versions of Docker Engine that do not include Docker Compose:
```bash
mkdir -p docs/env.d
cd docs
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/compose.yaml
curl -o env.d/common https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/common
curl -o env.d/backend https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/backend
curl -o env.d/yprovider https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/yprovider
curl -o env.d/common https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/postgresql
curl -o env.d/postgresql https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/postgresql
```
If you are using the sample nginx-proxy configuration:
```bash
curl -o default.conf.template https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docker/files/production/etc/nginx/conf.d/default.conf.template
```
## Step 2: Configuration

View File

@@ -168,9 +168,6 @@ DB_NAME: impress
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: impress
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
```
### Find s3 bucket connection values

View File

@@ -83,55 +83,6 @@ If you already have CRLF line endings in your local repository, the **best appro
git commit -m "✏️(project) Fix line endings to LF"
```
## Minio Permission Issues on Windows
### Problem Description
On Windows, you may encounter permission-related errors when running Minio in development mode with Docker Compose. This typically happens because:
- **Windows file permissions** don't map well to Unix-style user IDs used in Docker containers
- **Docker Desktop** may have issues with user mapping when using the `DOCKER_USER` environment variable
- **Minio container** fails to start or access volumes due to permission conflicts
### Common Symptoms
- Minio container fails to start with permission denied errors
- Error messages related to file system permissions in Minio logs
- Unable to create or access buckets in the development environment
- Docker Compose showing Minio service as unhealthy or exited
### Solution for Windows Users
If you encounter Minio permission issues on Windows, you can temporarily disable user mapping for the Minio service:
1. **Open the `compose.yml` file**
2. **Comment out the user directive** in the `minio` service section:
```yaml
minio:
# user: ${DOCKER_USER:-1000} # Comment this line on Windows if permission issues occur
image: minio/minio
environment:
- MINIO_ROOT_USER=impress
- MINIO_ROOT_PASSWORD=password
# ... rest of the configuration
```
3. **Restart the services**:
```bash
make run
```
### Why This Works
- Commenting out the `user` directive allows the Minio container to run with its default user
- This bypasses Windows-specific permission mapping issues
- The container will have the necessary permissions to access and manage the mounted volumes
### Note
This is a **development-only workaround**. In production environments, proper user mapping and security considerations should be maintained according to your deployment requirements.
## Frontend File Watching Issues on Windows
### Problem Description

View File

@@ -1,4 +1,4 @@
Y_PROVIDER_API_BASE_URL=http://${YPROVIDER_HOST}:4444/api
Y_PROVIDER_API_BASE_URL=http://${YPROVIDER_HOST}:4444/api/
Y_PROVIDER_API_KEY=<generate a random key>
COLLABORATION_SERVER_SECRET=<generate a random key>
COLLABORATION_SERVER_ORIGIN=https://${DOCS_HOST}

View File

@@ -66,7 +66,6 @@ class ListDocumentSerializer(serializers.ModelSerializer):
"""Serialize documents with limited fields for display in lists."""
is_favorite = serializers.BooleanField(read_only=True)
is_masked = serializers.BooleanField(read_only=True)
nb_accesses_ancestors = serializers.IntegerField(read_only=True)
nb_accesses_direct = serializers.IntegerField(read_only=True)
user_role = serializers.SerializerMethodField(read_only=True)
@@ -86,7 +85,6 @@ class ListDocumentSerializer(serializers.ModelSerializer):
"depth",
"excerpt",
"is_favorite",
"is_masked",
"link_role",
"link_reach",
"nb_accesses_ancestors",
@@ -109,7 +107,6 @@ class ListDocumentSerializer(serializers.ModelSerializer):
"depth",
"excerpt",
"is_favorite",
"is_masked",
"link_role",
"link_reach",
"nb_accesses_ancestors",
@@ -179,7 +176,6 @@ class DocumentSerializer(ListDocumentSerializer):
"depth",
"excerpt",
"is_favorite",
"is_masked",
"link_role",
"link_reach",
"nb_accesses_ancestors",

View File

@@ -1,6 +1,7 @@
"""API endpoints"""
# pylint: disable=too-many-lines
import base64
import json
import logging
import uuid
@@ -37,6 +38,15 @@ from rest_framework.throttling import UserRateThrottle
from core import authentication, choices, enums, models
from core.services.ai_services import AIService
from core.services.collaboration_services import CollaborationService
from core.services.converter_services import (
ServiceUnavailableError as YProviderServiceUnavailableError,
)
from core.services.converter_services import (
ValidationError as YProviderValidationError,
)
from core.services.converter_services import (
YdocConverter,
)
from core.tasks.mail import send_ask_for_access_mail
from core.utils import extract_attachments, filter_descendants
@@ -405,7 +415,6 @@ class DocumentViewSet(
queryset = super().filter_queryset(queryset)
user = self.request.user
queryset = queryset.annotate_is_favorite(user)
queryset = queryset.annotate_is_masked(user)
queryset = queryset.annotate_user_roles(user)
return queryset
@@ -454,9 +463,8 @@ class DocumentViewSet(
)
queryset = queryset.filter(path__in=root_paths)
# Annotate favorite and masked status and filter if applicable as late as possible
# Annotate favorite status and filter if applicable as late as possible
queryset = queryset.annotate_is_favorite(user)
queryset = queryset.annotate_is_masked(user)
for field in ["is_favorite", "is_masked"]:
queryset = filterset.filters[field].filter(queryset, filter_data[field])
@@ -1479,6 +1487,69 @@ class DocumentViewSet(
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@drf.decorators.action(
detail=True,
methods=["get"],
url_path="content",
name="Get document content in different formats",
)
def content(self, request, pk=None):
"""
Retrieve document content in different formats (JSON, Markdown, HTML).
Query parameters:
- content_format: The desired output format (json, markdown, html)
Returns:
JSON response with content in the specified format.
"""
document = self.get_object()
content_format = request.query_params.get("content_format", "json").lower()
if content_format not in {"json", "markdown", "html"}:
raise drf.exceptions.ValidationError(
"Invalid format. Must be one of: json, markdown, html"
)
# Get the base64 content from the document
content = None
base64_content = document.content
if base64_content is not None:
# Convert using the y-provider service
try:
yprovider = YdocConverter()
result = yprovider.convert(
base64.b64decode(base64_content),
"application/vnd.yjs.doc",
{
"markdown": "text/markdown",
"html": "text/html",
"json": "application/json",
}[content_format],
)
content = result
except YProviderValidationError as e:
return drf_response.Response(
{"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
)
except YProviderServiceUnavailableError as e:
logger.error("Error getting content for document %s: %s", pk, e)
return drf_response.Response(
{"error": "Failed to get document content"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
return drf_response.Response(
{
"id": str(document.id),
"title": document.title,
"content": content,
"created_at": document.created_at,
"updated_at": document.updated_at,
}
)
class DocumentAccessViewSet(
ResourceAccessViewsetMixin,

View File

@@ -326,18 +326,6 @@ class DocumentQuerySet(MP_NodeQuerySet):
return self.annotate(is_favorite=models.Value(False))
def annotate_is_masked(self, user):
"""
Annotate document queryset with the masked status for the current user.
"""
if user.is_authenticated:
masked_exists_subquery = LinkTrace.objects.filter(
document_id=models.OuterRef("pk"), user=user, is_masked=True
)
return self.annotate(is_masked=models.Exists(masked_exists_subquery))
return self.annotate(is_masked=models.Value(False))
def annotate_user_roles(self, user):
"""
Annotate document queryset with the roles of the current user
@@ -798,6 +786,7 @@ class Document(MP_Node, BaseModel):
"children_list": can_get,
"children_create": can_update and user.is_authenticated,
"collaboration_auth": can_get,
"content": can_get,
"cors_proxy": can_get,
"descendants": can_get,
"destroy": is_owner,

View File

@@ -1,4 +1,4 @@
"""Converter services."""
"""Y-Provider API services."""
from base64 import b64encode
@@ -28,25 +28,44 @@ class YdocConverter:
# Note: Yprovider microservice accepts only raw token, which is not recommended
return f"Bearer {settings.Y_PROVIDER_API_KEY}"
def convert(self, text):
def _request(self, url, data, content_type, accept):
"""Make a request to the Y-Provider API."""
response = requests.post(
url,
data=data,
headers={
"Authorization": self.auth_header,
"Content-Type": content_type,
"Accept": accept,
},
timeout=settings.CONVERSION_API_TIMEOUT,
verify=settings.CONVERSION_API_SECURE,
)
response.raise_for_status()
return response
def convert(
self, text, content_type="text/markdown", accept="application/vnd.yjs.doc"
):
"""Convert a Markdown text into our internal format using an external microservice."""
if not text:
raise ValidationError("Input text cannot be empty")
try:
response = requests.post(
response = self._request(
f"{settings.Y_PROVIDER_API_BASE_URL}{settings.CONVERSION_API_ENDPOINT}/",
data=text,
headers={
"Authorization": self.auth_header,
"Content-Type": "text/markdown",
},
timeout=settings.CONVERSION_API_TIMEOUT,
verify=settings.CONVERSION_API_SECURE,
text,
content_type,
accept,
)
response.raise_for_status()
return b64encode(response.content).decode("utf-8")
if accept == "application/vnd.yjs.doc":
return b64encode(response.content).decode("utf-8")
if accept in {"text/markdown", "text/html"}:
return response.text
if accept == "application/json":
return response.json()
raise ValidationError("Unsupported format")
except requests.RequestException as err:
raise ServiceUnavailableError(
"Failed to connect to conversion service",

View File

@@ -45,7 +45,6 @@ def test_api_documents_children_list_anonymous_public_standalone(
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -68,7 +67,6 @@ def test_api_documents_children_list_anonymous_public_standalone(
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -121,7 +119,6 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -144,7 +141,6 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -216,7 +212,6 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -239,7 +234,6 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -297,7 +291,6 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -320,7 +313,6 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -405,7 +397,6 @@ def test_api_documents_children_list_authenticated_related_direct(
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -428,7 +419,6 @@ def test_api_documents_children_list_authenticated_related_direct(
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -489,7 +479,6 @@ def test_api_documents_children_list_authenticated_related_parent(
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -512,7 +501,6 @@ def test_api_documents_children_list_authenticated_related_parent(
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -625,7 +613,6 @@ def test_api_documents_children_list_authenticated_related_team_members(
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -648,7 +635,6 @@ def test_api_documents_children_list_authenticated_related_team_members(
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,

View File

@@ -0,0 +1,176 @@
"""
Tests for Documents API endpoint in impress's core app: content
"""
import base64
from unittest.mock import patch
import pytest
import requests
from rest_framework import status
from rest_framework.test import APIClient
from core import factories
pytestmark = pytest.mark.django_db
@pytest.mark.parametrize(
"reach, role",
[
("public", "reader"),
("public", "editor"),
],
)
@patch("core.services.converter_services.YdocConverter.convert")
def test_api_documents_content_public(mock_content, reach, role):
"""Anonymous users should be allowed to access content of public documents."""
document = factories.DocumentFactory(link_reach=reach, link_role=role)
mock_content.return_value = {"some": "data"}
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/content/")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["id"] == str(document.id)
assert data["title"] == document.title
assert data["content"] == {"some": "data"}
mock_content.assert_called_once_with(
base64.b64decode(document.content),
"application/vnd.yjs.doc",
"application/json",
)
@pytest.mark.parametrize(
"reach, doc_role, user_role",
[
("restricted", "reader", "reader"),
("restricted", "reader", "editor"),
("restricted", "reader", "administrator"),
("restricted", "reader", "owner"),
("restricted", "editor", "reader"),
("restricted", "editor", "editor"),
("restricted", "editor", "administrator"),
("restricted", "editor", "owner"),
("authenticated", "reader", None),
("authenticated", "editor", None),
],
)
@patch("core.services.converter_services.YdocConverter.convert")
def test_api_documents_content_not_public(mock_content, reach, doc_role, user_role):
"""Authenticated users need access to get non-public document content."""
user = factories.UserFactory()
document = factories.DocumentFactory(link_reach=reach, link_role=doc_role)
mock_content.return_value = {"some": "data"}
# First anonymous request should fail
client = APIClient()
response = client.get(f"/api/v1.0/documents/{document.id!s}/content/")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
mock_content.assert_not_called()
# Login and try again
client.force_login(user)
response = client.get(f"/api/v1.0/documents/{document.id!s}/content/")
# If restricted, we still should not have access
if user_role is not None:
assert response.status_code == status.HTTP_403_FORBIDDEN
mock_content.assert_not_called()
# Create an access as a reader. This should unlock the access.
factories.UserDocumentAccessFactory(
document=document, user=user, role=user_role
)
response = client.get(f"/api/v1.0/documents/{document.id!s}/content/")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["id"] == str(document.id)
assert data["title"] == document.title
assert data["content"] == {"some": "data"}
mock_content.assert_called_once_with(
base64.b64decode(document.content),
"application/vnd.yjs.doc",
"application/json",
)
@pytest.mark.parametrize(
"content_format, accept",
[
("markdown", "text/markdown"),
("html", "text/html"),
("json", "application/json"),
],
)
@patch("core.services.converter_services.YdocConverter.convert")
def test_api_documents_content_format(mock_content, content_format, accept):
"""Test that the content endpoint returns a specific format."""
document = factories.DocumentFactory(link_reach="public")
mock_content.return_value = {"some": "data"}
response = APIClient().get(
f"/api/v1.0/documents/{document.id!s}/content/?content_format={content_format}"
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["id"] == str(document.id)
assert data["title"] == document.title
assert data["content"] == {"some": "data"}
mock_content.assert_called_once_with(
base64.b64decode(document.content), "application/vnd.yjs.doc", accept
)
@patch("core.services.converter_services.YdocConverter._request")
def test_api_documents_content_invalid_format(mock_request):
"""Test that the content endpoint rejects invalid formats."""
document = factories.DocumentFactory(link_reach="public")
response = APIClient().get(
f"/api/v1.0/documents/{document.id!s}/content/?content_format=invalid"
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
mock_request.assert_not_called()
@patch("core.services.converter_services.YdocConverter._request")
def test_api_documents_content_yservice_error(mock_request):
"""Test that service errors are handled properly."""
document = factories.DocumentFactory(link_reach="public")
mock_request.side_effect = requests.RequestException()
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/content/")
mock_request.assert_called_once()
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
@patch("core.services.converter_services.YdocConverter._request")
def test_api_documents_content_nonexistent_document(mock_request):
"""Test that accessing a nonexistent document returns 404."""
client = APIClient()
response = client.get(
"/api/v1.0/documents/00000000-0000-0000-0000-000000000000/content/"
)
assert response.status_code == status.HTTP_404_NOT_FOUND
mock_request.assert_not_called()
@patch("core.services.converter_services.YdocConverter._request")
def test_api_documents_content_empty_document(mock_request):
"""Test that accessing an empty document returns empty content."""
document = factories.DocumentFactory(link_reach="public", content="")
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/content/")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["id"] == str(document.id)
assert data["title"] == document.title
assert data["content"] is None
mock_request.assert_not_called()

View File

@@ -42,7 +42,6 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 1,
@@ -67,7 +66,6 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
"is_favorite": False,
"is_masked": False,
"link_reach": grand_child.link_reach,
"link_role": grand_child.link_role,
"numchild": 0,
@@ -90,7 +88,6 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -142,7 +139,6 @@ def test_api_documents_descendants_list_anonymous_public_parent():
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 1,
@@ -165,7 +161,6 @@ def test_api_documents_descendants_list_anonymous_public_parent():
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
"is_favorite": False,
"is_masked": False,
"link_reach": grand_child.link_reach,
"link_role": grand_child.link_role,
"numchild": 0,
@@ -188,7 +183,6 @@ def test_api_documents_descendants_list_anonymous_public_parent():
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -261,7 +255,6 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 1,
@@ -284,7 +277,6 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
"is_favorite": False,
"is_masked": False,
"link_reach": grand_child.link_reach,
"link_role": grand_child.link_role,
"numchild": 0,
@@ -307,7 +299,6 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -365,7 +356,6 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 1,
@@ -388,7 +378,6 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
"is_favorite": False,
"is_masked": False,
"link_reach": grand_child.link_reach,
"link_role": grand_child.link_role,
"numchild": 0,
@@ -411,7 +400,6 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -490,7 +478,6 @@ def test_api_documents_descendants_list_authenticated_related_direct():
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 1,
@@ -513,7 +500,6 @@ def test_api_documents_descendants_list_authenticated_related_direct():
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
"is_favorite": False,
"is_masked": False,
"link_reach": grand_child.link_reach,
"link_role": grand_child.link_role,
"numchild": 0,
@@ -536,7 +522,6 @@ def test_api_documents_descendants_list_authenticated_related_direct():
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -595,7 +580,6 @@ def test_api_documents_descendants_list_authenticated_related_parent():
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 1,
@@ -618,7 +602,6 @@ def test_api_documents_descendants_list_authenticated_related_parent():
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
"is_favorite": False,
"is_masked": False,
"link_reach": grand_child.link_reach,
"link_role": grand_child.link_role,
"numchild": 0,
@@ -641,7 +624,6 @@ def test_api_documents_descendants_list_authenticated_related_parent():
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -746,7 +728,6 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 1,
@@ -769,7 +750,6 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
"is_favorite": False,
"is_masked": False,
"link_reach": grand_child.link_reach,
"link_role": grand_child.link_role,
"numchild": 0,
@@ -792,7 +772,6 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_masked": False,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,

View File

@@ -72,7 +72,6 @@ def test_api_documents_list_format():
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": True,
"is_masked": False,
"link_reach": document.link_reach,
"link_role": document.link_role,
"nb_accesses_ancestors": 3,
@@ -409,7 +408,6 @@ def test_api_documents_list_favorites_no_extra_queries(django_assert_num_queries
assert len(results) == 5
assert all(result["is_favorite"] is False for result in results)
assert all(result["is_masked"] is False for result in results)
# Mark documents as favorite and check results again
for document in special_documents:
@@ -429,5 +427,3 @@ def test_api_documents_list_favorites_no_extra_queries(django_assert_num_queries
assert result["is_favorite"] is True
else:
assert result["is_favorite"] is False
# All documents should be unmasked in this test
assert result["is_masked"] is False

View File

@@ -37,6 +37,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"children_list": True,
"collaboration_auth": True,
"cors_proxy": True,
"content": True,
"descendants": True,
"destroy": False,
"duplicate": False,
@@ -113,6 +114,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": False,
# Anonymous user can't favorite a document even with read access
@@ -218,6 +220,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": True,
"favorite": True,
@@ -300,6 +303,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": True,
"favorite": True,
@@ -494,6 +498,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": access.role == "owner",
"duplicate": True,
"favorite": True,

View File

@@ -81,6 +81,7 @@ def test_api_documents_trashbin_format():
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": True,
"duplicate": True,
"favorite": True,

View File

@@ -161,6 +161,7 @@ def test_models_documents_get_abilities_forbidden(
"collaboration_auth": False,
"descendants": False,
"cors_proxy": False,
"content": False,
"destroy": False,
"duplicate": False,
"favorite": False,
@@ -224,6 +225,7 @@ def test_models_documents_get_abilities_reader(
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": is_authenticated,
"favorite": is_authenticated,
@@ -289,6 +291,7 @@ def test_models_documents_get_abilities_editor(
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": is_authenticated,
"favorite": is_authenticated,
@@ -343,6 +346,7 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": True,
"duplicate": True,
"favorite": True,
@@ -394,6 +398,7 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": True,
"favorite": True,
@@ -448,6 +453,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": True,
"favorite": True,
@@ -509,6 +515,7 @@ def test_models_documents_get_abilities_reader_user(
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": True,
"favorite": True,
@@ -568,6 +575,7 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": False,
"duplicate": True,
"favorite": True,

View File

@@ -1,4 +1,4 @@
"""Test converter services."""
"""Test y-provider services."""
from base64 import b64decode
from unittest.mock import MagicMock, patch
@@ -84,6 +84,42 @@ def test_convert_full_integration(mock_post, settings):
headers={
"Authorization": "Bearer test-key",
"Content-Type": "text/markdown",
"Accept": "application/vnd.yjs.doc",
},
timeout=5,
verify=False,
)
@patch("requests.post")
def test_convert_full_integration_with_specific_headers(mock_post, settings):
"""Test successful conversion with specific content type and accept headers."""
settings.Y_PROVIDER_API_BASE_URL = "http://test.com/"
settings.Y_PROVIDER_API_KEY = "test-key"
settings.CONVERSION_API_ENDPOINT = "conversion-endpoint"
settings.CONVERSION_API_TIMEOUT = 5
settings.CONVERSION_API_SECURE = False
converter = YdocConverter()
expected_response = "# Test Document\n\nThis is test content."
mock_response = MagicMock()
mock_response.text = expected_response
mock_response.raise_for_status.return_value = None
mock_post.return_value = mock_response
result = converter.convert(
b"test_content", "application/vnd.yjs.doc", "text/markdown"
)
assert result == expected_response
mock_post.assert_called_once_with(
"http://test.com/conversion-endpoint/",
data=b"test_content",
headers={
"Authorization": "Bearer test-key",
"Content-Type": "application/vnd.yjs.doc",
"Accept": "text/markdown",
},
timeout=5,
verify=False,

View File

@@ -16,6 +16,7 @@ from socket import gethostbyname, gethostname
from django.utils.translation import gettext_lazy as _
import dj_database_url
import sentry_sdk
from configurations import Configuration, values
from csp.constants import NONE
@@ -78,7 +79,9 @@ class Base(Configuration):
# Database
DATABASES = {
"default": {
"default": dj_database_url.config()
if os.environ.get("DATABASE_URL")
else {
"ENGINE": values.Value(
"django.db.backends.postgresql",
environ_name="DB_ENGINE",

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -43,6 +43,10 @@ msgid "Creator is me"
msgstr "Me eo an aozer"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Sinedoù"
@@ -66,7 +70,7 @@ msgstr "Doare korf"
msgid "Format"
msgstr "Stumm"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr "eilenn {title}"
@@ -225,8 +229,8 @@ msgstr "implijer"
msgid "users"
msgstr "implijerien"
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr "titl"
@@ -236,161 +240,161 @@ msgstr "bomm"
#: build/lib/core/models.py:418 core/models.py:418
msgid "Document"
msgstr "Teul"
msgstr "Restr"
#: build/lib/core/models.py:419 core/models.py:419
msgid "Documents"
msgstr "Teulioù"
msgstr "Restroù"
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr "Teuliad hep titl"
msgstr "Restr hep titl"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} en deus rannet un teul ganeoc'h!"
msgstr "{name} en deus rannet ur restr ganeoc'h!"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:860 core/models.py:860
#, 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 an teul da-heul:"
msgstr "{name} en deus pedet ac'hanoc'h gant ar rol \"{role}\" war ar restr da-heul:"
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:866 core/models.py:866
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} en deus rannet un teul ganeoc'h: {title}"
msgstr "{name} en deus rannet ur restr ganeoc'h: {title}"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link trace"
msgstr "Roud liamm an teuliad/an implijer"
msgstr "Roud liamm ar restr/an implijer"
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr "Roudoù liamm an teuliad/an implijer"
msgstr "Roudoù liamm ar restr/an implijer"
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:973 core/models.py:973
msgid "A link trace already exists for this document/user."
msgstr "Ur roud liamm a zo dija evit an teul/an implijer."
msgstr "Ur roud liamm a zo dija evit an restr/an implijer."
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorite"
msgstr "Teuliad muiañ-karet"
msgstr "Restr muiañ-karet"
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr "Teuliadoù muiañ-karet"
msgstr "Restroù muiañ-karet"
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1003 core/models.py:1003
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "An teul-mañ a zo un teul muiañ karet gant an implijer-mañ."
msgstr "Ar restr-mañ a zo ur restr muiañ karet gant an implijer-mañ."
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relation"
msgstr "Liamm teul/implijer"
msgstr "Liamm restr/implijer"
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr "Liammoù teul/implijer"
msgstr "Liammoù restr/implijer"
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr "An implijer-mañ a zo dija en teul-mañ."
msgstr "An implijer-mañ a zo dija er restr-mañ."
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1038 core/models.py:1038
msgid "This team is already in this document."
msgstr "Ar skipailh-mañ a zo dija en teul-mañ."
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for access"
msgstr "Goulenn tizhout an teul"
msgstr "Goulenn tizhout ar restr"
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr "Goulennoù tizhout an teul"
msgstr "Goulennoù tizhout ar restr"
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1197 core/models.py:1197
msgid "This user has already asked for access to this document."
msgstr "An implijer en deus goulennet tizhout an teul-mañ."
msgstr "An implijer en deus goulennet tizhout ar restr-mañ."
#: build/lib/core/models.py:1260 core/models.py:1260
#: 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 an teul-mañ!"
msgstr "{name} en defe c'hoant da dizhout ar restr-mañ!"
#: build/lib/core/models.py:1264 core/models.py:1264
#: 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 an teul da-heul:"
msgstr "{name} en defe c'hoant da dizhout ar restr da-heul:"
#: build/lib/core/models.py:1270 core/models.py:1270
#: 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 an teul: {title}"
msgstr "{name} en defe c'hoant da dizhout ar restr: {title}"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "description"
msgstr "deskrivadur"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr "kod"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr "publik"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1290 core/models.py:1290
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:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Template"
msgstr "Patrom"
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr "Patromoù"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr "Liamm patrom/implijer"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr "Liammoù patrom/implijer"
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr "An implijer-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr "postel"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "Pedadenn d'un teul"
msgstr "Pedadenn d'ur restr"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "Pedadennoù d'un teul"
msgstr "Pedadennoù d'ur restr"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1480 core/models.py:1480
msgid "This email is already associated to a registered user."
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
@@ -407,7 +411,7 @@ msgstr "Digeriñ"
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, hoc'h ostilh nevez ret-holl evit aozañ, rannañ ha kenlabourat war an teulioù e skipailh. "
msgstr " Docs, hoc'h ostilh nevez ret-holl evit aozañ, rannañ ha kenlabourat war ar restr e skipailh. "
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -43,6 +43,10 @@ msgid "Creator is me"
msgstr "Ersteller bin ich"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favorit"
@@ -66,7 +70,7 @@ msgstr "Typ"
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr "Kopie von {title}"
@@ -225,8 +229,8 @@ msgstr "Benutzer"
msgid "users"
msgstr "Benutzer"
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr "Titel"
@@ -242,155 +246,155 @@ msgstr "Dokument"
msgid "Documents"
msgstr "Dokumente"
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:860 core/models.py:860
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} hat Sie mit der Rolle \"{role}\" zu folgendem Dokument eingeladen:"
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:866 core/models.py:866
#, 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:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:973 core/models.py:973
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:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1003 core/models.py:1003
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:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1038 core/models.py:1038
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1197 core/models.py:1197
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: 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:1264 core/models.py:1264
#: 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:1270 core/models.py:1270
#: 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:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "description"
msgstr "Beschreibung"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr "Code"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr "öffentlich"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Whether this template is public for anyone to use."
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Template"
msgstr "Vorlage"
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr "Vorlagen"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr "Vorlage/Benutzer-Beziehung"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr "Vorlage/Benutzerbeziehungen"
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr "Dieses Team ist bereits in diesem Template."
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1480 core/models.py:1480
msgid "This email is already associated to a registered user."
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -43,6 +43,10 @@ msgid "Creator is me"
msgstr ""
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr ""
@@ -66,7 +70,7 @@ msgstr ""
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -225,8 +229,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr ""
@@ -242,155 +246,155 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:860 core/models.py:860
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:866 core/models.py:866
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:973 core/models.py:973
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1003 core/models.py:1003
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1038 core/models.py:1038
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1197 core/models.py:1197
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: 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:1264 core/models.py:1264
#: 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:1270 core/models.py:1270
#: 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:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "description"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr ""
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1480 core/models.py:1480
msgid "This email is already associated to a registered user."
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -43,6 +43,10 @@ msgid "Creator is me"
msgstr "Yo soy el creador"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favorito"
@@ -66,7 +70,7 @@ msgstr "Tipo de Cuerpo"
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr "copia de {title}"
@@ -225,8 +229,8 @@ msgstr "usuario"
msgid "users"
msgstr "usuarios"
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr "título"
@@ -242,155 +246,155 @@ msgstr "Documento"
msgid "Documents"
msgstr "Documentos"
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr "Documento sin título"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "¡{name} ha compartido un documento contigo!"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:860 core/models.py:860
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "Te ha invitado {name} al siguiente documento con el rol \"{role}\" :"
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:866 core/models.py:866
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha compartido un documento contigo: {title}"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link trace"
msgstr "Traza del enlace de documento/usuario"
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr "Trazas del enlace de documento/usuario"
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:973 core/models.py:973
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:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorite"
msgstr "Documento favorito"
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr "Documentos favoritos"
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1003 core/models.py:1003
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:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relation"
msgstr "Relación documento/usuario"
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr "Relaciones documento/usuario"
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr "Este usuario ya forma parte del documento."
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1038 core/models.py:1038
msgid "This team is already in this document."
msgstr "Este equipo ya forma parte del documento."
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1197 core/models.py:1197
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:1260 core/models.py:1260
#: 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:1264 core/models.py:1264
#: 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:1270 core/models.py:1270
#: 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:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "description"
msgstr "descripción"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr "código"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr "público"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1290 core/models.py:1290
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:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Template"
msgstr "Plantilla"
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr "Plantillas"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr "Relación plantilla/usuario"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr "Relaciones plantilla/usuario"
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr "Este usuario ya forma parte de la plantilla."
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr "Este equipo ya se encuentra en esta plantilla."
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr "dirección de correo electrónico"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1480 core/models.py:1480
msgid "This email is already associated to a registered user."
msgstr "Este correo electrónico está asociado a un usuario registrado."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -43,6 +43,10 @@ msgid "Creator is me"
msgstr "Je suis l'auteur"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favoris"
@@ -66,7 +70,7 @@ msgstr "Type de corps"
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr "copie de {title}"
@@ -225,8 +229,8 @@ msgstr "utilisateur"
msgid "users"
msgstr "utilisateurs"
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr "titre"
@@ -242,155 +246,155 @@ msgstr "Document"
msgid "Documents"
msgstr "Documents"
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr "Document sans titre"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} a partagé un document avec vous!"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:860 core/models.py:860
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} vous a invité avec le rôle \"{role}\" sur le document suivant :"
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:866 core/models.py:866
#, 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:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link trace"
msgstr "Trace du lien document/utilisateur"
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr "Traces du lien document/utilisateur"
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:973 core/models.py:973
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:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorite"
msgstr "Document favori"
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr "Documents favoris"
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1003 core/models.py:1003
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:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relation"
msgstr "Relation document/utilisateur"
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr "Relations document/utilisateur"
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr "Cet utilisateur est déjà dans ce document."
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1038 core/models.py:1038
msgid "This team is already in this document."
msgstr "Cette équipe est déjà dans ce document."
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for access"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1197 core/models.py:1197
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:1260 core/models.py:1260
#: 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:1264 core/models.py:1264
#: 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:1270 core/models.py:1270
#: 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:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "description"
msgstr "description"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr "public"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1290 core/models.py:1290
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:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Template"
msgstr "Modèle"
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr "Modèles"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr "Relation modèle/utilisateur"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr "Relations modèle/utilisateur"
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr "Cet utilisateur est déjà dans ce modèle."
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr "Cette équipe est déjà modèle."
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr "adresse e-mail"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "Invitation à un document"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "Invitations à un document"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1480 core/models.py:1480
msgid "This email is already associated to a registered user."
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -43,6 +43,10 @@ msgid "Creator is me"
msgstr "Il creatore sono io"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Preferiti"
@@ -66,7 +70,7 @@ msgstr ""
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr "copia di {title}"
@@ -225,8 +229,8 @@ msgstr "utente"
msgid "users"
msgstr "utenti"
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr "titolo"
@@ -242,155 +246,155 @@ msgstr "Documento"
msgid "Documents"
msgstr "Documenti"
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr "Documento senza titolo"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ha condiviso un documento con te!"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:860 core/models.py:860
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} ti ha invitato con il ruolo \"{role}\" nel seguente documento:"
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:866 core/models.py:866
#, 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:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:973 core/models.py:973
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorite"
msgstr "Documento preferito"
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr "Documenti preferiti"
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1003 core/models.py:1003
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr "Questo utente è già presente in questo documento."
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1038 core/models.py:1038
msgid "This team is already in this document."
msgstr "Questo team è già presente in questo documento."
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1197 core/models.py:1197
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: 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:1264 core/models.py:1264
#: 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:1270 core/models.py:1270
#: 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:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "description"
msgstr "descrizione"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr "pubblico"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Whether this template is public for anyone to use."
msgstr "Indica se questo modello è pubblico per chiunque."
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Template"
msgstr "Modello"
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr "Modelli"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr "Questo utente è già in questo modello."
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr "Questo team è già in questo modello."
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1480 core/models.py:1480
msgid "This email is already associated to a registered user."
msgstr "Questa email è già associata a un utente registrato."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -43,6 +43,10 @@ msgid "Creator is me"
msgstr "Ik ben Eigenaar"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favoriete"
@@ -66,7 +70,7 @@ msgstr "Text type"
msgid "Format"
msgstr "Formaat"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr "kopie van {title}"
@@ -225,8 +229,8 @@ msgstr "gebruiker"
msgid "users"
msgstr "gebruikers"
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr "titel"
@@ -242,155 +246,155 @@ msgstr "Document"
msgid "Documents"
msgstr "Documenten"
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr "Naamloos Document"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} heeft een document met gedeeld!"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:860 core/models.py:860
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} heeft u uitgenodigd met de rol \"{role}\" op het volgende document:"
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:866 core/models.py:866
#, 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:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link trace"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:973 core/models.py:973
msgid "A link trace already exists for this document/user."
msgstr "Een url bestaat al voor dit document/deze gebruiker."
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorite"
msgstr "Document favoriet"
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr "Document favorieten"
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1003 core/models.py:1003
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Dit document is al in gebruik als favoriete door dezelfde gebruiker."
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relation"
msgstr "Document/gebruiker relatie"
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr "Document/gebruiker relaties"
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr "De gebruiker is al in dit document."
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1038 core/models.py:1038
msgid "This team is already in this document."
msgstr "Het team is al in dit document."
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1197 core/models.py:1197
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: 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:1264 core/models.py:1264
#: 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:1270 core/models.py:1270
#: 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:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "description"
msgstr "omschrijving"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr "publiek"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Whether this template is public for anyone to use."
msgstr "Of dit template als publiek is en door iedereen te gebruiken is."
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Template"
msgstr "Template"
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr "Templates"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr "Template/gebruiker relatie"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr "Template/gebruiker relaties"
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr "De gebruiker bestaat al in dit template."
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr "Het team bestaat al in dit template."
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr "email adres"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1480 core/models.py:1480
msgid "This email is already associated to a registered user."
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -43,6 +43,10 @@ msgid "Creator is me"
msgstr "Eu sou o criador"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favorito"
@@ -66,7 +70,7 @@ msgstr "Tipo de corpo"
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr "cópia de {title}"
@@ -225,8 +229,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr ""
@@ -242,155 +246,155 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:860 core/models.py:860
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:866 core/models.py:866
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:973 core/models.py:973
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1003 core/models.py:1003
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1038 core/models.py:1038
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1197 core/models.py:1197
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: 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:1264 core/models.py:1264
#: 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:1270 core/models.py:1270
#: 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:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "description"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr ""
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1480 core/models.py:1480
msgid "This email is already associated to a registered user."
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -43,6 +43,10 @@ msgid "Creator is me"
msgstr "Ustvaril sem jaz"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Priljubljena"
@@ -66,7 +70,7 @@ msgstr "Vrsta telesa"
msgid "Format"
msgstr "Oblika"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -225,8 +229,8 @@ msgstr "uporabnik"
msgid "users"
msgstr "uporabniki"
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr "naslov"
@@ -242,155 +246,155 @@ msgstr "Dokument"
msgid "Documents"
msgstr "Dokumenti"
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr "Dokument brez naslova"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} je delil dokument z vami!"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:860 core/models.py:860
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} vas je povabil z vlogo \"{role}\" na naslednjem dokumentu:"
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:866 core/models.py:866
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} je delil dokument z vami: {title}"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link trace"
msgstr "Dokument/sled povezave uporabnika"
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr "Sledi povezav dokumenta/uporabnika"
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:973 core/models.py:973
msgid "A link trace already exists for this document/user."
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorite"
msgstr "Priljubljeni dokument"
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr "Priljubljeni dokumenti"
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1003 core/models.py:1003
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:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relation"
msgstr "Odnos dokument/uporabnik"
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr "Odnosi dokument/uporabnik"
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr "Ta uporabnik je že v tem dokumentu."
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1038 core/models.py:1038
msgid "This team is already in this document."
msgstr "Ta ekipa je že v tem dokumentu."
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1197 core/models.py:1197
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: 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:1264 core/models.py:1264
#: 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:1270 core/models.py:1270
#: 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:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "description"
msgstr "opis"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr "koda"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr "javno"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Whether this template is public for anyone to use."
msgstr "Ali je ta predloga javna za uporabo."
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Template"
msgstr "Predloga"
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr "Predloge"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr "Odnos predloga/uporabnik"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr "Odnosi med predlogo in uporabnikom"
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr "Ta uporabnik je že v tej predlogi."
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr "Ta ekipa je že v tej predlogi."
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1480 core/models.py:1480
msgid "This email is already associated to a registered user."
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -43,6 +43,10 @@ msgid "Creator is me"
msgstr "Skaparen är jag"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favoriter"
@@ -66,7 +70,7 @@ msgstr ""
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -225,8 +229,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr ""
@@ -242,155 +246,155 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:860 core/models.py:860
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:866 core/models.py:866
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:973 core/models.py:973
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1003 core/models.py:1003
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1038 core/models.py:1038
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1197 core/models.py:1197
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: 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:1264 core/models.py:1264
#: 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:1270 core/models.py:1270
#: 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:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "description"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr ""
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1480 core/models.py:1480
msgid "This email is already associated to a registered user."
msgstr "Denna e-postadress är redan associerad med en registrerad användare."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -43,6 +43,10 @@ msgid "Creator is me"
msgstr ""
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr ""
@@ -66,7 +70,7 @@ msgstr ""
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -225,8 +229,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr ""
@@ -242,155 +246,155 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:860 core/models.py:860
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:866 core/models.py:866
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:973 core/models.py:973
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1003 core/models.py:1003
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1038 core/models.py:1038
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1197 core/models.py:1197
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: 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:1264 core/models.py:1264
#: 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:1270 core/models.py:1270
#: 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:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "description"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr ""
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1480 core/models.py:1480
msgid "This email is already associated to a registered user."
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-18 10:25\n"
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -43,6 +43,10 @@ msgid "Creator is me"
msgstr "创建者是我"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "收藏"
@@ -66,7 +70,7 @@ msgstr "正文类型"
msgid "Format"
msgstr "格式"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
@@ -225,8 +229,8 @@ msgstr "用户"
msgid "users"
msgstr "个用户"
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr "标题"
@@ -242,155 +246,155 @@ msgstr "文档"
msgid "Documents"
msgstr "个文档"
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr "未命名文档"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} 与您共享了一个文档!"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:860 core/models.py:860
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:866 core/models.py:866
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} 与您共享了一个文档:{title}"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link trace"
msgstr "文档/用户链接跟踪"
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr "个文档/用户链接跟踪"
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:973 core/models.py:973
msgid "A link trace already exists for this document/user."
msgstr "此文档/用户的链接跟踪已存在。"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorite"
msgstr "文档收藏"
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr "文档收藏夹"
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1003 core/models.py:1003
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "该文档已被同一用户的收藏关系实例关联。"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relation"
msgstr "文档/用户关系"
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr "文档/用户关系集"
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr "该用户已在此文档中。"
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1038 core/models.py:1038
msgid "This team is already in this document."
msgstr "该团队已在此文档中。"
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
msgid "Either user or team must be set, not both."
msgstr "必须设置用户或团队之一,不能同时设置两者。"
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1197 core/models.py:1197
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: 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:1264 core/models.py:1264
#: 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:1270 core/models.py:1270
#: 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:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "description"
msgstr "说明"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr "代码"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr "公开"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Whether this template is public for anyone to use."
msgstr "该模板是否公开供任何人使用。"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Template"
msgstr "模板"
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr "模板"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr "模板/用户关系"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr "模板/用户关系集"
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr "该用户已在此模板中。"
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr "该团队已在此模板中。"
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr "电子邮件地址"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "文档邀请"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "文档邀请"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1480 core/models.py:1480
msgid "This email is already associated to a registered user."
msgstr "此电子邮件已经与现有注册用户关联。"

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "3.4.2"
version = "3.5.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -29,6 +29,7 @@ dependencies = [
"boto3==1.39.4",
"Brotli==1.1.0",
"celery[redis]==5.5.3",
"dj-database-url==2.3.0",
"django-configurations==2.5.1",
"django-cors-headers==4.7.0",
"django-countries==7.6.1",

View File

@@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('/');
await expect(
page.locator('header').first().locator('h2').getByText('Docs'),
page.locator('header').first().locator('h1').getByText('Docs'),
).toBeVisible();
await page.goto('unknown-page404');
});

View File

@@ -23,7 +23,7 @@ const saveStorageState = async (
page.locator('header').first().getByRole('button', {
name: 'Logout',
}),
).toBeVisible();
).toBeVisible({ timeout: 10000 });
await page.context().storageState({
path: storageState as string,

View File

@@ -22,13 +22,57 @@ test.describe('Doc Create', () => {
);
const header = page.locator('header').first();
await header.locator('h2').getByText('Docs').click();
await header.locator('h1').getByText('Docs').click();
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid).toBeVisible();
await expect(page.getByTestId('grid-loader')).toBeHidden();
await expect(docsGrid.getByText(docTitle)).toBeVisible();
});
test('it creates a sub doc from slash menu editor', async ({
page,
browserName,
}) => {
const [title] = await createDoc(page, 'my-new-slash-doc', browserName, 1);
await verifyDocName(page, title);
await page.locator('.bn-block-outer').last().fill('/');
await page
.getByText('New sub-doc', {
exact: true,
})
.click();
const input = page.getByRole('textbox', { name: 'doc title input' });
await expect(input).toHaveText('');
await expect(
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
).toBeVisible();
});
test('it creates a sub doc from interlinking dropdown', async ({
page,
browserName,
}) => {
const [title] = await createDoc(page, 'my-new-slash-doc', browserName, 1);
await verifyDocName(page, title);
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Link a doc').first().click();
await page
.locator('.quick-search-container')
.getByText('New sub-doc')
.click();
const input = page.getByRole('textbox', { name: 'doc title input' });
await expect(input).toHaveText('');
await expect(
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
).toBeVisible();
});
});
test.describe('Doc Create: Not logged', () => {

View File

@@ -706,4 +706,75 @@ test.describe('Doc Editor', () => {
'pink',
);
});
test('it checks interlink feature', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-interlink', browserName, 1);
await verifyDocName(page, randomDoc);
const { name: docChild1 } = await createRootSubPage(
page,
browserName,
'doc-interlink-child-1',
);
await verifyDocName(page, docChild1);
const { name: docChild2 } = await createRootSubPage(
page,
browserName,
'doc-interlink-child-2',
);
await verifyDocName(page, docChild2);
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Link a doc').first().click();
const input = page.locator(
"span[data-inline-content-type='interlinkingSearchInline'] input",
);
const searchContainer = page.locator('.quick-search-container');
await input.fill('doc-interlink');
await expect(searchContainer.getByText(randomDoc)).toBeVisible();
await expect(searchContainer.getByText(docChild1)).toBeVisible();
await expect(searchContainer.getByText(docChild2)).toBeVisible();
await input.pressSequentially('-child');
await expect(searchContainer.getByText(docChild1)).toBeVisible();
await expect(searchContainer.getByText(docChild2)).toBeVisible();
await expect(searchContainer.getByText(randomDoc)).toBeHidden();
// use keydown to select the second result
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
const interlink = page.getByRole('link', {
name: 'child-2',
});
await expect(interlink).toBeVisible();
await interlink.click();
await verifyDocName(page, docChild2);
});
test('it checks interlink shortcut @', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-interlink', browserName, 1);
await verifyDocName(page, randomDoc);
const editor = page.locator('.bn-block-outer').last();
await editor.click();
await page.keyboard.press('@');
await expect(
page.locator(
"span[data-inline-content-type='interlinkingSearchInline'] input",
),
).toBeVisible();
});
});

View File

@@ -4,7 +4,14 @@ import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
import pdf from 'pdf-parse';
import { createDoc, verifyDocName } from './utils-common';
import {
TestLanguage,
createDoc,
randomName,
verifyDocName,
waitForLanguageSwitch,
} from './utils-common';
import { createRootSubPage } from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -411,4 +418,139 @@ test.describe('Doc Export', () => {
expect(pdfData.text).toContain('Column 2');
expect(pdfData.text).toContain('Column 3');
});
test('it injects the correct language attribute into PDF export', async ({
page,
browserName,
}) => {
await waitForLanguageSwitch(page, TestLanguage.French);
// Wait for the page to be ready after language switch
await page.waitForLoadState('domcontentloaded');
const header = page.locator('header').first();
await header.locator('h1').getByText('Docs').click();
const randomDocFrench = randomName(
'doc-language-export-french',
browserName,
1,
)[0];
await page
.getByRole('button', {
name: 'Nouveau doc',
})
.click();
await page.waitForURL('**/docs/**', {
timeout: 10000,
waitUntil: 'domcontentloaded',
});
const input = page.getByLabel('doc title input');
await expect(input).toBeVisible();
await expect(input).toHaveText('');
await input.click();
await input.fill(randomDocFrench);
await input.blur();
const editor = page.locator('.ProseMirror.bn-editor');
await editor.click();
await editor.fill('Contenu de test pour export en français');
await page
.getByRole('button', {
name: 'download',
exact: true,
})
.click();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDocFrench}.pdf`);
});
void page
.getByRole('button', {
name: 'Télécharger',
exact: true,
})
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDocFrench}.pdf`);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfString = pdfBuffer.toString('latin1');
expect(pdfString).toContain('/Lang (fr)');
});
test('it exports the doc with interlinking', async ({
page,
browserName,
}) => {
const [randomDoc] = await createDoc(
page,
'export-interlinking',
browserName,
1,
);
await verifyDocName(page, randomDoc);
const { name: docChild } = await createRootSubPage(
page,
browserName,
'export-interlink-child',
);
await verifyDocName(page, docChild);
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Link a doc').first().click();
await page
.locator(
"span[data-inline-content-type='interlinkingSearchInline'] input",
)
.fill('interlink-child');
await page
.locator('.quick-search-container')
.getByText('interlink-child')
.click();
const interlink = page.getByRole('link', {
name: 'interlink-child',
});
await expect(interlink).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${docChild}.pdf`);
});
await page
.getByRole('button', {
name: 'download',
exact: true,
})
.click();
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${docChild}.pdf`);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfData = await pdf(pdfBuffer);
expect(pdfData.text).toContain('interlink-child'); // This is the pdf text
});
});

View File

@@ -1,15 +1,16 @@
import { expect, test } from '@playwright/test';
import { createDoc, mockedListDocs } from './utils-common';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Doc grid dnd', () => {
test('it creates a doc', async ({ page, browserName }) => {
await page.goto('/');
const header = page.locator('header').first();
await createDoc(page, 'Draggable doc', browserName, 1);
await header.locator('h2').getByText('Docs').click();
await header.locator('h1').getByText('Docs').click();
await createDoc(page, 'Droppable doc', browserName, 1);
await header.locator('h2').getByText('Docs').click();
await header.locator('h1').getByText('Docs').click();
const response = await page.waitForResponse(
(response) =>
@@ -165,6 +166,40 @@ test.describe('Doc grid dnd', () => {
});
});
test.describe('Doc grid dnd mobile', () => {
test.use({ viewport: { width: 500, height: 1200 } });
test('DND is deactivated on mobile', async ({ page, browserName }) => {
await page.goto('/');
const docsGrid = page.getByTestId('docs-grid');
await expect(page.getByTestId('docs-grid')).toBeVisible();
await expect(page.getByTestId('grid-loader')).toBeHidden();
await expect(docsGrid.getByRole('row').first()).toBeVisible();
await expect(docsGrid.locator('.--docs--grid-droppable')).toHaveCount(0);
await createDoc(page, 'Draggable doc mobile', browserName, 1, true);
await createRootSubPage(
page,
browserName,
'Draggable doc mobile child',
true,
);
await page
.getByRole('button', { name: 'Open the header menu' })
.getByText('menu')
.click();
await expect(page.locator('.--docs-sub-page-item').first()).toHaveAttribute(
'draggable',
'false',
);
});
});
const data = [
{
id: 'can-drop-and-drag',

View File

@@ -119,7 +119,7 @@ test.describe('Document grid item options', () => {
await page.getByText('push_pin').click();
// Check is pinned
await expect(row.getByLabel('Pin document icon')).toBeVisible();
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
@@ -128,7 +128,7 @@ test.describe('Document grid item options', () => {
await page.getByText('Unpin').click();
// Check is unpinned
await expect(row.getByLabel('Pin document icon')).toBeHidden();
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
});
@@ -227,18 +227,18 @@ test.describe('Documents filters', () => {
// Initial state
await expect(allDocs).toBeVisible();
await expect(allDocs).toHaveAttribute('aria-selected', 'true');
await expect(allDocs).toHaveAttribute('aria-current', 'page');
await expect(myDocs).toBeVisible();
await expect(myDocs).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
await expect(myDocs).toHaveAttribute('aria-selected', 'false');
await expect(myDocs).not.toHaveAttribute('aria-current');
await expect(sharedWithMe).toBeVisible();
await expect(sharedWithMe).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)',
);
await expect(sharedWithMe).toHaveAttribute('aria-selected', 'false');
await expect(sharedWithMe).not.toHaveAttribute('aria-current');
await allDocs.click();

View File

@@ -409,7 +409,7 @@ test.describe('Doc Header', () => {
const row = await getGridRow(page, docTitle);
// Check is pinned
await expect(row.getByLabel('Pin document icon')).toBeVisible();
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
@@ -424,7 +424,7 @@ test.describe('Doc Header', () => {
await page.goto('/');
// Check is unpinned
await expect(row.getByLabel('Pin document icon')).toBeHidden();
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
});

View File

@@ -244,9 +244,7 @@ test.describe('Document create member: Multiple login', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible();
await expect(page.getByTestId('header-logo-link')).toBeVisible();
await page.goto(urlDoc);
@@ -271,9 +269,7 @@ test.describe('Document create member: Multiple login', () => {
await page.goto('/');
await keyCloakSignIn(page, browserName);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible({
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
});
@@ -334,9 +330,7 @@ test.describe('Document create member: Multiple login', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible({
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
});

View File

@@ -16,18 +16,11 @@ test.describe('Doc Routing', () => {
await page.goto('/');
});
test('Check the presence of the meta tag noindex', async ({ page }) => {
const buttonCreateHomepage = page.getByRole('button', {
name: 'New doc',
});
await expect(buttonCreateHomepage).toBeVisible();
await buttonCreateHomepage.click();
await expect(
page.getByRole('button', {
name: 'Share',
}),
).toBeVisible();
test('Check the presence of the meta tag noindex', async ({
page,
browserName,
}) => {
await createDoc(page, 'doc-routing-test', browserName, 1);
const metaDescription = page.locator('meta[name="robots"]');
await expect(metaDescription).toHaveAttribute('content', 'noindex');
});

View File

@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { createDoc, randomName, verifyDocName } from './utils-common';
import { createDoc, verifyDocName } from './utils-common';
import { createRootSubPage } from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
@@ -26,10 +26,7 @@ test.describe('Document search', () => {
);
await verifyDocName(page, doc2Title);
await page.goto('/');
await page
.getByTestId('left-panel-desktop')
.getByRole('button', { name: 'search' })
.click();
await page.getByTestId('search-docs-button').click();
await expect(
page.getByRole('img', { name: 'No active search' }),
@@ -104,9 +101,7 @@ test.describe('Document search', () => {
browserName,
}) => {
// Doc grid filters are not visible
const searchButton = page
.getByTestId('left-panel-desktop')
.getByRole('button', { name: 'search', exact: true });
const searchButton = page.getByTestId('search-docs-button');
const filters = page.getByTestId('doc-search-filters');
@@ -163,24 +158,14 @@ test.describe('Document search', () => {
await verifyDocName(page, firstDocTitle);
// Create a new doc - for the moment without children
await page.getByRole('button', { name: 'New doc' }).click();
await verifyDocName(page, '');
await page.getByRole('textbox', { name: 'doc title input' }).click();
await page
.getByRole('textbox', { name: 'doc title input' })
.press('ControlOrMeta+a');
const [secondDocTitle] = randomName(
const [secondDocTitle] = await createDoc(
page,
'My second sub page search',
browserName,
1,
);
await page
.getByRole('textbox', { name: 'doc title input' })
.fill(secondDocTitle);
const searchButton = page
.getByTestId('left-panel-desktop')
.getByRole('button', { name: 'search' });
const searchButton = page.getByTestId('search-docs-button');
await searchButton.click();
await page.getByRole('combobox', { name: 'Quick search input' }).click();
@@ -199,16 +184,23 @@ test.describe('Document search', () => {
await page.getByRole('button', { name: 'close' }).click();
// Create a sub page
await createRootSubPage(page, browserName, secondDocTitle);
const { name: secondChildDocTitle } = await createRootSubPage(
page,
browserName,
'second - Child doc',
);
await searchButton.click();
await page
.getByRole('combobox', { name: 'Quick search input' })
.fill('sub page search');
.fill('second');
// Now there is a sub page - expect to have the focus on the current doc
await expect(
page.getByRole('presentation').getByLabel(secondDocTitle),
).toBeVisible();
await expect(
page.getByRole('presentation').getByLabel(secondChildDocTitle),
).toBeVisible();
await expect(
page.getByRole('presentation').getByLabel(firstDocTitle),
).toBeHidden();

View File

@@ -25,7 +25,7 @@ test.describe('Doc Tree', () => {
1,
);
await verifyDocName(page, titleParent);
const addButton = page.getByRole('button', { name: 'New doc' });
const addButton = page.getByTestId('new-doc-button');
const docTree = page.getByTestId('doc-tree');
await expect(addButton).toBeVisible();
@@ -63,7 +63,7 @@ test.describe('Doc Tree', () => {
test('check the reorder of sub pages', async ({ page, browserName }) => {
await createDoc(page, 'doc-tree-content', browserName, 1);
const addButton = page.getByRole('button', { name: 'New doc' });
const addButton = page.getByTestId('new-doc-button');
await expect(addButton).toBeVisible();
const docTree = page.getByTestId('doc-tree');
@@ -201,7 +201,7 @@ test.describe('Doc Tree', () => {
).not.toHaveText(docChild);
const header = page.locator('header').first();
await header.locator('h2').getByText('Docs').click();
await header.locator('h1').getByText('Docs').click();
await expect(page.getByText(docChild)).toBeVisible();
});

View File

@@ -122,9 +122,7 @@ test.describe('Doc Visibility: Restricted', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible({
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
});
@@ -178,9 +176,7 @@ test.describe('Doc Visibility: Restricted', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible();
await expect(page.getByTestId('header-logo-link')).toBeVisible();
await page.goto(urlDoc);
@@ -246,8 +242,8 @@ test.describe('Doc Visibility: Public', () => {
cardContainer.getByText('Public document', { exact: true }),
).toBeVisible();
await expect(page.getByRole('button', { name: 'search' })).toBeVisible();
await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible();
await expect(page.getByTestId('search-docs-button')).toBeVisible();
await expect(page.getByTestId('new-doc-button')).toBeVisible();
const urlDoc = page.url();
@@ -262,8 +258,8 @@ test.describe('Doc Visibility: Public', () => {
await page.goto(urlDoc);
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
await expect(page.getByRole('button', { name: 'search' })).toBeHidden();
await expect(page.getByRole('button', { name: 'New doc' })).toBeHidden();
await expect(page.getByTestId('search-docs-button')).toBeHidden();
await expect(page.getByTestId('new-doc-button')).toBeHidden();
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
const card = page.getByLabel('It is the card information');
await expect(card).toBeVisible();
@@ -455,9 +451,7 @@ test.describe('Doc Visibility: Authenticated', () => {
const otherBrowser = BROWSERS.find((b) => b !== browserName);
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible({
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
});
@@ -545,9 +539,7 @@ test.describe('Doc Visibility: Authenticated', () => {
const otherBrowser = BROWSERS.find((b) => b !== browserName);
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible();
await expect(page.getByTestId('header-logo-link')).toBeVisible();
await page.goto(urlDoc);

View File

@@ -12,8 +12,8 @@ test.describe('Header', () => {
const header = page.locator('header').first();
await expect(header.getByLabel('Docs Logo')).toBeVisible();
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
await expect(header.getByTestId('header-logo-link')).toBeVisible();
await expect(header.locator('h1').getByText('Docs')).toHaveCSS(
'font-family',
/Roboto/i,
);
@@ -37,8 +37,8 @@ test.describe('Header', () => {
const header = page.locator('header').first();
await expect(header.getByLabel('Docs Logo')).toBeVisible();
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
await expect(header.locator('h1').getByText('Docs')).toHaveCSS(
'font-family',
/Marianne/i,
);
@@ -106,7 +106,7 @@ test.describe('Header mobile', () => {
const header = page.locator('header').first();
await expect(header.getByLabel('Open the header menu')).toBeVisible();
await expect(header.getByRole('link', { name: 'Docs Logo' })).toBeVisible();
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
await expect(
header.getByRole('button', {
name: 'Les services de La Suite numérique',

View File

@@ -15,10 +15,13 @@ test.describe('Home page', () => {
const header = page.locator('header').first();
const footer = page.locator('footer').first();
await expect(header).toBeVisible();
await expect(
header.getByRole('button', { name: /Language/ }),
).toBeVisible();
await expect(header.getByRole('img', { name: 'Docs logo' })).toBeVisible();
const languageButton = page.getByRole('button', {
name: /Language|Select language/,
});
await expect(languageButton).toBeVisible();
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
// Check the titles
@@ -65,20 +68,31 @@ test.describe('Home page', () => {
await page.goto('/docs/');
// Wait for the page to be fully loaded and responsive store to be initialized
await page.waitForLoadState('domcontentloaded');
// Wait a bit more for the responsive store to be initialized
await page.waitForTimeout(500);
// Check header content
const header = page.locator('header').first();
const footer = page.locator('footer').first();
await expect(header).toBeVisible();
await expect(
header.getByRole('button', { name: /Language/ }),
).toBeVisible();
// Check for language picker - it should be visible on desktop
// Use a more flexible selector that works with both Header and HomeHeader
const languageButton = page.getByRole('button', {
name: /Language|Select language/,
});
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();
await expect(header.getByRole('img', { name: 'Docs logo' })).toBeVisible();
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
// Check the titles

View File

@@ -1,30 +1,17 @@
import { Page, expect, test } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { createDoc } from './utils-common';
test.describe.serial('Language', () => {
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.afterAll(async () => {
await page.close();
});
import { TestLanguage, createDoc, waitForLanguageSwitch } from './utils-common';
test.describe('Language', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await waitForLanguageSwitch(page, TestLanguage.English);
});
test.afterEach(async ({ page }) => {
// Switch back to English - important for other tests to run as expected
await waitForLanguageSwitch(page, TestLanguage.English);
});
test('checks language switching', async ({ page }) => {
const header = page.locator('header').first();
const languagePicker = header.locator('.--docs--language-picker-text');
await expect(page.locator('html')).toHaveAttribute('lang', 'en-us');
// initial language should be english
await expect(
@@ -36,17 +23,57 @@ test.describe.serial('Language', () => {
// switch to french
await waitForLanguageSwitch(page, TestLanguage.French);
await expect(page.locator('html')).toHaveAttribute('lang', 'fr');
await expect(
header.getByRole('button').getByText('Français'),
).toBeVisible();
await expect(page.getByLabel('Se déconnecter')).toBeVisible();
await header.getByRole('button').getByText('Français').click();
await page.getByLabel('Deutsch').click();
// Switch to German using the utility function for consistency
await waitForLanguageSwitch(page, TestLanguage.German);
await expect(header.getByRole('button').getByText('Deutsch')).toBeVisible();
await expect(page.getByLabel('Abmelden')).toBeVisible();
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
await languagePicker.click();
await expect(page.locator('[role="menu"]')).toBeVisible();
const menuItems = page.getByRole('menuitem');
await expect(menuItems.first()).toBeVisible();
await menuItems.first().click();
await expect(page.locator('html')).toHaveAttribute('lang', 'en');
await expect(languagePicker).toContainText('English');
});
test('can switch language using only keyboard', async ({ page }) => {
await page.goto('/');
await waitForLanguageSwitch(page, TestLanguage.English);
const languagePicker = page.getByRole('button', {
name: /select language/i,
});
await expect(languagePicker).toBeVisible();
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
const menu = page.getByRole('menu');
await expect(menu).toBeVisible();
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
await expect(page.locator('html')).not.toHaveAttribute('lang', 'en-us');
});
test('checks that backend uses the same language as the frontend', async ({
@@ -94,48 +121,3 @@ test.describe.serial('Language', () => {
await expect(page.getByText('Titres', { exact: true })).toBeVisible();
});
});
// language helper
export const TestLanguage = {
English: {
label: 'English',
expectedLocale: ['en-us'],
},
French: {
label: 'Français',
expectedLocale: ['fr-fr'],
},
German: {
label: 'Deutsch',
expectedLocale: ['de-de'],
},
} as const;
type TestLanguageKey = keyof typeof TestLanguage;
type TestLanguageValue = (typeof TestLanguage)[TestLanguageKey];
export async function waitForLanguageSwitch(
page: Page,
lang: TestLanguageValue,
) {
const header = page.locator('header').first();
const languagePicker = header.locator('.--docs--language-picker-text');
const isAlreadyTargetLanguage = await languagePicker
.innerText()
.then((text) => text.toLowerCase().includes(lang.label.toLowerCase()));
if (isAlreadyTargetLanguage) {
return;
}
await languagePicker.click();
const responsePromise = page.waitForResponse(
(resp) =>
resp.url().includes('/user') && resp.request().method() === 'PATCH',
);
await page.getByLabel(lang.label).click();
const resolvedResponsePromise = await responsePromise;
const responseData = await resolvedResponsePromise.json();
expect(lang.expectedLocale).toContain(responseData.language);
}

View File

@@ -8,8 +8,8 @@ test.describe('Left panel desktop', () => {
test('checks all the elements are visible', async ({ page }) => {
await expect(page.getByTestId('left-panel-desktop')).toBeVisible();
await expect(page.getByTestId('left-panel-mobile')).toBeHidden();
await expect(page.getByRole('button', { name: 'house' })).toBeVisible();
await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible();
await expect(page.getByTestId('home-button')).toBeVisible();
await expect(page.getByTestId('new-doc-button')).toBeVisible();
});
});
@@ -27,9 +27,11 @@ test.describe('Left panel mobile', () => {
await expect(page.getByTestId('left-panel-mobile')).not.toBeInViewport();
const header = page.locator('header').first();
const homeButton = page.getByRole('button', { name: 'house' });
const newDocButton = page.getByRole('button', { name: 'New doc' });
const languageButton = page.getByRole('button', { name: /Language/ });
const homeButton = page.getByTestId('home-button');
const newDocButton = page.getByTestId('new-doc-button');
const languageButton = page.getByRole('button', {
name: 'Select language',
});
const logoutButton = page.getByRole('button', { name: 'Logout' });
await expect(homeButton).not.toBeInViewport();

View File

@@ -78,14 +78,16 @@ export const createDoc = async (
docName: string,
browserName: string,
length: number = 1,
isChild: boolean = false,
isMobile: boolean = false,
) => {
const randomDocs = randomName(docName, browserName, length);
for (let i = 0; i < randomDocs.length; i++) {
if (!isChild) {
const header = page.locator('header').first();
await header.locator('h2').getByText('Docs').click();
if (isMobile) {
await page
.getByRole('button', { name: 'Open the header menu' })
.getByText('menu')
.click();
}
await page
@@ -152,7 +154,7 @@ export const goToGridDoc = async (
{ nthRow = 1, title }: GoToGridDocOptions = {},
) => {
const header = page.locator('header').first();
await header.locator('h2').getByText('Docs').click();
await header.locator('h1').getByText('Docs').click();
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid).toBeVisible();
@@ -271,3 +273,52 @@ export const expectLoginPage = async (page: Page) =>
).toBeVisible({
timeout: 10000,
});
// language helper
export const TestLanguage = {
English: {
label: 'English',
expectedLocale: ['en-us'],
},
French: {
label: 'Français',
expectedLocale: ['fr-fr'],
},
German: {
label: 'Deutsch',
expectedLocale: ['de-de'],
},
} as const;
type TestLanguageKey = keyof typeof TestLanguage;
type TestLanguageValue = (typeof TestLanguage)[TestLanguageKey];
export async function waitForLanguageSwitch(
page: Page,
lang: TestLanguageValue,
) {
await page.route('**/api/v1.0/users/**', async (route, request) => {
if (request.method().includes('PATCH')) {
await route.fulfill({
json: {
language: lang.expectedLocale[0],
},
});
} else {
await route.continue();
}
});
const header = page.locator('header').first();
const languagePicker = header.locator('.--docs--language-picker-text');
const isAlreadyTargetLanguage = await languagePicker
.innerText()
.then((text) => text.toLowerCase().includes(lang.label.toLowerCase()));
if (isAlreadyTargetLanguage) {
return;
}
await languagePicker.click();
await page.getByLabel(lang.label).click();
}

View File

@@ -10,7 +10,15 @@ export const createRootSubPage = async (
page: Page,
browserName: string,
docName: string,
isMobile: boolean = false,
) => {
if (isMobile) {
await page
.getByRole('button', { name: 'Open the header menu' })
.getByText('menu')
.click();
}
// Get response
const responsePromise = waitForResponseCreateDoc(page);
await clickOnAddRootSubPage(page);
@@ -18,6 +26,13 @@ export const createRootSubPage = async (
expect(response.ok()).toBeTruthy();
const subPageJson = (await response.json()) as { id: string };
if (isMobile) {
await page
.getByRole('button', { name: 'Open the header menu' })
.getByText('menu')
.click();
}
// Get doc tree
const docTree = page.getByTestId('doc-tree');
await expect(docTree).toBeVisible();
@@ -29,6 +44,13 @@ export const createRootSubPage = async (
await expect(subPageItem).toBeVisible();
await subPageItem.click();
if (isMobile) {
await page
.getByRole('button', { name: 'Open the header menu' })
.getByText('close')
.click();
}
// Update sub page name
const randomDocs = randomName(docName, browserName, 1);
await updateDocTitle(page, randomDocs[0]);

View File

@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "3.4.2",
"version": "3.5.0",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
@@ -12,7 +12,7 @@
"test:ui::chromium": "yarn test:ui --project=chromium"
},
"devDependencies": {
"@playwright/test": "1.54.1",
"@playwright/test": "1.54.2",
"@types/node": "*",
"@types/pdf-parse": "1.1.5",
"eslint-config-impress": "*",

View File

@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "3.4.2",
"version": "3.5.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -29,12 +29,12 @@
"@emoji-mart/react": "1.1.1",
"@fontsource/material-icons": "5.2.5",
"@gouvfr-lasuite/integration": "1.0.3",
"@gouvfr-lasuite/ui-kit": "0.10.0",
"@gouvfr-lasuite/ui-kit": "0.11.0",
"@hocuspocus/provider": "2.15.2",
"@openfun/cunningham-react": "3.2.0",
"@openfun/cunningham-react": "3.2.1",
"@react-pdf/renderer": "4.3.0",
"@sentry/nextjs": "9.42.0",
"@tanstack/react-query": "5.83.0",
"@sentry/nextjs": "10.2.0",
"@tanstack/react-query": "5.84.1",
"canvg": "4.0.3",
"clsx": "2.1.1",
"cmdk": "1.1.1",
@@ -46,8 +46,8 @@
"idb": "8.0.3",
"lodash": "4.17.21",
"luxon": "3.7.1",
"next": "15.4.4",
"posthog-js": "1.258.2",
"next": "15.4.6",
"posthog-js": "1.258.6",
"react": "*",
"react-aria-components": "1.11.0",
"react-dom": "*",
@@ -58,22 +58,22 @@
"use-debounce": "10.0.5",
"y-protocols": "1.0.6",
"yjs": "*",
"zustand": "5.0.6"
"zustand": "5.0.7"
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.83.0",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "6.6.3",
"@tanstack/react-query-devtools": "5.84.1",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.6.4",
"@testing-library/react": "16.3.0",
"@testing-library/user-event": "14.6.1",
"@types/jest": "30.0.0",
"@types/lodash": "4.17.20",
"@types/luxon": "3.6.2",
"@types/luxon": "3.7.1",
"@types/node": "*",
"@types/react": "*",
"@types/react-dom": "*",
"cross-env": "7.0.3",
"cross-env": "10.0.0",
"dotenv": "17.2.1",
"eslint-config-impress": "*",
"fetch-mock": "9.11.0",
@@ -81,11 +81,11 @@
"jest-environment-jsdom": "30.0.5",
"node-fetch": "2.7.0",
"prettier": "3.6.2",
"stylelint": "16.22.0",
"stylelint-config-standard": "38.0.0",
"stylelint": "16.23.0",
"stylelint-config-standard": "39.0.0",
"stylelint-prettier": "5.0.3",
"typescript": "*",
"webpack": "5.100.2",
"webpack": "5.101.0",
"workbox-webpack-plugin": "7.1.0"
}
}

View File

@@ -16,6 +16,7 @@ export interface BoxProps {
$background?: CSSProperties['background'];
$color?: CSSProperties['color'];
$css?: string | RuleSet<object>;
$cursor?: CSSProperties['cursor'];
$direction?: CSSProperties['flexDirection'];
$display?: CSSProperties['display'];
$effect?: 'show' | 'hide';
@@ -44,13 +45,13 @@ export interface BoxProps {
export type BoxType = ComponentPropsWithRef<typeof Box>;
export const Box = styled('div')<BoxProps>`
display: flex;
flex-direction: column;
${({ $align }) => $align && `align-items: ${$align};`}
${({ $background }) => $background && `background: ${$background};`}
${({ $color }) => $color && `color: ${$color};`}
${({ $direction }) => $direction && `flex-direction: ${$direction};`}
${({ $display }) => $display && `display: ${$display};`}
${({ $cursor }) => $cursor && `cursor: ${$cursor};`}
${({ $direction }) => `flex-direction: ${$direction || 'column'};`}
${({ $display, as }) =>
`display: ${$display || as?.match('span|input') ? 'inline-flex' : 'flex'};`}
${({ $flex }) => $flex && `flex: ${$flex};`}
${({ $gap }) => $gap && `gap: ${$gap};`}
${({ $height }) => $height && `height: ${$height};`}

View File

@@ -31,11 +31,11 @@ const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
$background="none"
$margin="none"
$padding="none"
$hasTransition
$css={css`
cursor: ${props.disabled ? 'not-allowed' : 'pointer'};
border: none;
outline: none;
transition: all 0.2s ease-in-out;
font-family: inherit;
color: ${props.disabled

View File

@@ -33,6 +33,13 @@ const StyledButton = styled(Button)<StyledButtonProps>`
font-size: 0.938rem;
padding: 0;
${({ $css }) => $css};
&:focus-visible {
outline: 2px solid var(--c--theme--colors--primary-500);
outline-offset: 2px;
border-radius: 4px;
transition: none;
}
`;
export interface DropButtonProps {

View File

@@ -1,10 +1,19 @@
import { HorizontalSeparator } from '@gouvfr-lasuite/ui-kit';
import { Fragment, PropsWithChildren, useRef, useState } from 'react';
import {
Fragment,
PropsWithChildren,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { css } from 'styled-components';
import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
export type DropdownMenuOption = {
icon?: string;
label: string;
@@ -25,6 +34,7 @@ export type DropdownMenuProps = {
arrowCss?: BoxProps['$css'];
buttonCss?: BoxProps['$css'];
disabled?: boolean;
opened?: boolean;
topMessage?: string;
selectedValues?: string[];
afterOpenChange?: (isOpen: boolean) => void;
@@ -38,18 +48,47 @@ export const DropdownMenu = ({
arrowCss,
buttonCss,
label,
opened,
topMessage,
afterOpenChange,
selectedValues,
}: PropsWithChildren<DropdownMenuProps>) => {
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const [isOpen, setIsOpen] = useState(false);
const [isOpen, setIsOpen] = useState(opened ?? false);
const [focusedIndex, setFocusedIndex] = useState(-1);
const blockButtonRef = useRef<HTMLDivElement>(null);
const menuItemRefs = useRef<(HTMLDivElement | null)[]>([]);
const onOpenChange = (isOpen: boolean) => {
setIsOpen(isOpen);
afterOpenChange?.(isOpen);
};
const onOpenChange = useCallback(
(isOpen: boolean) => {
setIsOpen(isOpen);
setFocusedIndex(-1);
afterOpenChange?.(isOpen);
},
[afterOpenChange],
);
useDropdownKeyboardNav({
isOpen,
focusedIndex,
options,
menuItemRefs,
setFocusedIndex,
onOpenChange,
});
// Focus selected menu item when menu opens
useEffect(() => {
if (isOpen && menuItemRefs.current.length > 0) {
const selectedIndex = options.findIndex((option) => option.isSelected);
if (selectedIndex !== -1) {
setFocusedIndex(selectedIndex);
setTimeout(() => {
menuItemRefs.current[selectedIndex]?.focus();
}, 0);
}
}
}, [isOpen, options]);
if (disabled) {
return children;
@@ -93,6 +132,7 @@ export const DropdownMenu = ({
$maxWidth="320px"
$minWidth={`${blockButtonRef.current?.clientWidth}px`}
role="menu"
aria-label={label}
>
{topMessage && (
<Text
@@ -113,14 +153,20 @@ export const DropdownMenu = ({
return;
}
const isDisabled = option.disabled !== undefined && option.disabled;
const isFocused = index === focusedIndex;
return (
<Fragment key={option.label}>
<BoxButton
ref={(el) => {
menuItemRefs.current[index] = el;
}}
role="menuitem"
aria-label={option.label}
data-testid={option.testId}
$direction="row"
disabled={isDisabled}
$hasTransition={false}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
@@ -156,6 +202,19 @@ export const DropdownMenu = ({
&:hover {
background-color: var(--c--theme--colors--greyscale-050);
}
&:focus-visible {
outline: 2px solid var(--c--theme--colors--primary-500);
outline-offset: -2px;
background-color: var(--c--theme--colors--greyscale-050);
}
${isFocused &&
css`
outline: 2px solid var(--c--theme--colors--primary-500);
outline-offset: -2px;
background-color: var(--c--theme--colors--greyscale-050);
`}
`}
>
<Box

View File

@@ -0,0 +1,88 @@
import { RefObject, useEffect } from 'react';
import { DropdownMenuOption } from '../DropdownMenu';
type UseDropdownKeyboardNavProps = {
isOpen: boolean;
focusedIndex: number;
options: DropdownMenuOption[];
menuItemRefs: RefObject<(HTMLDivElement | null)[]>;
setFocusedIndex: (index: number) => void;
onOpenChange: (isOpen: boolean) => void;
};
export const useDropdownKeyboardNav = ({
isOpen,
focusedIndex,
options,
menuItemRefs,
setFocusedIndex,
onOpenChange,
}: UseDropdownKeyboardNavProps) => {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (!isOpen) {
return;
}
const enabledIndices = options
.map((option, index) =>
option.show !== false && !option.disabled ? index : -1,
)
.filter((index) => index !== -1);
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
const nextIndex =
focusedIndex < enabledIndices.length - 1 ? focusedIndex + 1 : 0;
const nextEnabledIndex = enabledIndices[nextIndex];
setFocusedIndex(nextIndex);
menuItemRefs.current[nextEnabledIndex]?.focus();
break;
case 'ArrowUp':
event.preventDefault();
const prevIndex =
focusedIndex > 0 ? focusedIndex - 1 : enabledIndices.length - 1;
const prevEnabledIndex = enabledIndices[prevIndex];
setFocusedIndex(prevIndex);
menuItemRefs.current[prevEnabledIndex]?.focus();
break;
case 'Enter':
case ' ':
event.preventDefault();
if (focusedIndex >= 0 && focusedIndex < enabledIndices.length) {
const selectedOptionIndex = enabledIndices[focusedIndex];
const selectedOption = options[selectedOptionIndex];
if (selectedOption && selectedOption.callback) {
onOpenChange(false);
void selectedOption.callback();
}
}
break;
case 'Escape':
event.preventDefault();
onOpenChange(false);
break;
}
};
if (isOpen) {
document.addEventListener('keydown', handleKeyDown);
}
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [
isOpen,
focusedIndex,
options,
menuItemRefs,
setFocusedIndex,
onOpenChange,
]);
};

View File

@@ -1,9 +1,12 @@
import { css } from 'styled-components';
import { Box } from '../Box';
import { DropdownMenu, DropdownMenuOption } from '../DropdownMenu';
import { Icon } from '../Icon';
import { Text } from '../Text';
import {
DropdownMenu,
DropdownMenuOption,
} from '../dropdown-menu/DropdownMenu';
export type FilterDropdownProps = {
options: DropdownMenuOption[];

View File

@@ -3,7 +3,8 @@ export * from './Box';
export * from './BoxButton';
export * from './Card';
export * from './DropButton';
export * from './DropdownMenu';
export * from './dropdown-menu/DropdownMenu';
export * from './quick-search';
export * from './Icon';
export * from './InfiniteScroll';
export * from './Link';

View File

@@ -1,5 +1,11 @@
import { Command } from 'cmdk';
import { ReactNode, useRef } from 'react';
import {
PropsWithChildren,
ReactNode,
useEffect,
useRef,
useState,
} from 'react';
import { hasChildrens } from '@/utils/children';
@@ -30,7 +36,6 @@ export type QuickSearchProps = {
loading?: boolean;
label?: string;
placeholder?: string;
children?: ReactNode;
};
export const QuickSearch = ({
@@ -42,14 +47,47 @@ export const QuickSearch = ({
label,
placeholder,
children,
}: QuickSearchProps) => {
}: PropsWithChildren<QuickSearchProps>) => {
const ref = useRef<HTMLDivElement | null>(null);
const [selectedValue, setSelectedValue] = useState<string>('');
// Auto-select first item when children change
useEffect(() => {
if (!children) {
setSelectedValue('');
return;
}
// Small delay for DOM to update
const timeoutId = setTimeout(() => {
const firstItem = ref.current?.querySelector('[cmdk-item]');
if (firstItem) {
const value =
firstItem.getAttribute('data-value') ||
firstItem.getAttribute('value') ||
firstItem.textContent?.trim() ||
'';
if (value) {
setSelectedValue(value);
}
}
}, 50);
return () => clearTimeout(timeoutId);
}, [children]);
return (
<>
<QuickSearchStyle />
<div className="quick-search-container">
<Command label={label} shouldFilter={false} ref={ref}>
<Command
label={label}
shouldFilter={false}
ref={ref}
value={selectedValue}
onValueChange={setSelectedValue}
tabIndex={0}
>
{showInput && (
<QuickSearchInput
loading={loading}

View File

@@ -1,7 +1,7 @@
import { Command } from 'cmdk';
import { ReactNode } from 'react';
import { Box } from '../Box';
import { Box, Text } from '@/components';
import { QuickSearchData } from './QuickSearch';
import { QuickSearchItem } from './QuickSearchItem';
@@ -23,6 +23,7 @@ export const QuickSearchGroup = <T,>({
key={group.groupName}
heading={group.groupName}
forceMount={false}
contentEditable={false}
>
{group.startActions?.map((action, index) => {
return (
@@ -58,7 +59,13 @@ export const QuickSearchGroup = <T,>({
);
})}
{group.emptyString && group.elements.length === 0 && (
<span className="ml-b clr-greyscale-500">{group.emptyString}</span>
<Text
$variation="500"
$margin={{ left: '2xs', bottom: '3xs' }}
$size="sm"
>
{group.emptyString}
</Text>
)}
</Command.Group>
</Box>

View File

@@ -1,133 +1,136 @@
import { createGlobalStyle } from 'styled-components';
export const QuickSearchStyle = createGlobalStyle`
& *:focus-visible {
outline: none;
}
.quick-search-container {
[cmdk-root] {
width: 100%;
background: #ffffff;
border-radius: 12px;
overflow: hidden;
transition: transform 100ms ease;
outline: none;
}
[cmdk-input] {
border: none;
width: 100%;
font-size: 17px;
padding: 8px;
background: white;
outline: none;
color: var(--c--theme--colors--greyscale-1000);
border-radius: 0;
&::placeholder {
color: var(--c--theme--colors--greyscale-500);
}
}
[cmdk-item] {
content-visibility: auto;
cursor: pointer;
border-radius: var(--c--theme--spacings--xs);
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
user-select: none;
will-change: background, color;
transition: all 150ms ease;
transition-property: none;
.show-right-on-focus {
opacity: 0;
width: 100%;
background: #ffffff;
border-radius: 12px;
overflow: hidden;
transition: transform 100ms ease;
outline: none;
}
&:hover,
&[data-selected='true'] {
background: var(--c--theme--colors--greyscale-100);
.show-right-on-focus {
opacity: 1;
[cmdk-input] {
border: none;
width: 100%;
font-size: 17px;
padding: 8px;
background: white;
outline: none;
color: var(--c--theme--colors--greyscale-1000);
border-radius: 0;
&::placeholder {
color: var(--c--theme--colors--greyscale-500);
}
}
&[data-disabled='true'] {
color: var(--c--theme--colors--greyscale-500);
cursor: not-allowed;
}
& + [cmdk-item] {
margin-top: 4px;
}
}
[cmdk-list] {
flex:1;
overflow-y: auto;
overscroll-behavior: contain;
}
[cmdk-vercel-shortcuts] {
display: flex;
margin-left: auto;
gap: 8px;
kbd {
font-size: 12px;
min-width: 20px;
padding: 4px;
height: 20px;
border-radius: 4px;
color: white;
background: var(--c--theme--colors--greyscale-500);
display: inline-flex;
[cmdk-item] {
content-visibility: auto;
cursor: pointer;
border-radius: var(--c--theme--spacings--xs);
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
text-transform: uppercase;
gap: 8px;
user-select: none;
will-change: background, color;
transition: all 150ms ease;
transition-property: none;
.show-right-on-focus {
opacity: 0;
}
&:hover,
&[data-selected='true'] {
background: var(--c--theme--colors--greyscale-100);
.show-right-on-focus {
opacity: 1;
}
}
&[data-disabled='true'] {
color: var(--c--theme--colors--greyscale-500);
cursor: not-allowed;
}
& + [cmdk-item] {
margin-top: 4px;
}
}
[cmdk-list] {
flex: 1;
overflow-y: auto;
overscroll-behavior: contain;
}
[cmdk-vercel-shortcuts] {
display: flex;
margin-left: auto;
gap: 8px;
kbd {
font-size: 12px;
min-width: 20px;
padding: 4px;
height: 20px;
border-radius: 4px;
color: white;
background: var(--c--theme--colors--greyscale-500);
display: inline-flex;
align-items: center;
justify-content: center;
text-transform: uppercase;
}
}
[cmdk-separator] {
height: 1px;
width: 100%;
background: var(--c--theme--colors--greyscale-500);
margin: 4px 0;
}
*:not([hidden]) + [cmdk-group] {
margin-top: 8px;
}
[cmdk-group-heading] {
user-select: none;
font-size: var(--c--theme--font--sizes--sm);
color: var(--c--theme--colors--greyscale-700);
font-weight: bold;
display: flex;
align-items: center;
margin-bottom: var(--c--theme--spacings--xs);
}
[cmdk-empty] {
}
}
[cmdk-separator] {
height: 1px;
width: 100%;
background: var(--c--theme--colors--greyscale-500);
margin: 4px 0;
.c__modal__scroller:has(.quick-search-container),
.c__modal__scroller:has(.noPadding) {
padding: 0 !important;
.c__modal__close .c__button {
right: 5px;
top: 5px;
padding: 1.5rem 1rem;
}
.c__modal__title {
font-size: var(--c--theme--font--sizes--xs);
padding: var(--c--theme--spacings--base);
margin-bottom: 0;
}
}
*:not([hidden]) + [cmdk-group] {
margin-top: 8px;
}
[cmdk-group-heading] {
user-select: none;
font-size: var(--c--theme--font--sizes--sm);
color: var(--c--theme--colors--greyscale-700);
font-weight: bold;
display: flex;
align-items: center;
margin-bottom: var(--c--theme--spacings--xs);
}
[cmdk-empty] {
}
}
.c__modal__scroller:has(.quick-search-container),
.c__modal__scroller:has(.noPadding) {
padding: 0 !important;
.c__modal__close .c__button {
right: 5px;
top: 5px;
padding: 1.5rem 1rem;
}
.c__modal__title {
font-size: var(--c--theme--font--sizes--xs);
padding: var(--c--theme--spacings--base);
margin-bottom: 0;
}
}
`;

View File

@@ -0,0 +1,3 @@
<svg width="18" height="23" viewBox="0 0 18 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.57954 5.93282C4.44486 5.79021 4.37793 5.61484 4.37793 5.41299C4.37793 5.21112 4.44495 5.03795 4.58154 4.90136C4.72456 4.75833 4.90437 4.6875 5.11367 4.6875H12.8964C13.0991 4.6875 13.2722 4.75857 13.408 4.90133C13.5508 5.03718 13.6219 5.21027 13.6219 5.41299C13.6219 5.61613 13.5506 5.7918 13.409 5.93387C13.273 6.07732 13.0996 6.14873 12.8964 6.14873H5.11367C4.90437 6.14873 4.72456 6.0779 4.58154 5.93487L4.57954 5.93282ZM4.57954 9.51144C4.44486 9.36882 4.37793 9.19346 4.37793 8.9916C4.37793 8.78973 4.44495 8.61656 4.58154 8.47997C4.72456 8.33695 4.90437 8.26611 5.11367 8.26611H12.8964C13.0991 8.26611 13.2722 8.33719 13.408 8.47995C13.5508 8.61579 13.6219 8.78888 13.6219 8.9916C13.6219 9.19475 13.5506 9.37042 13.409 9.51249C13.273 9.65593 13.0996 9.72734 12.8964 9.72734H5.11367C4.90437 9.72734 4.72456 9.65651 4.58154 9.51348L4.57954 9.51144ZM4.57954 13.1003C4.44561 12.9585 4.37793 12.7869 4.37793 12.5907C4.37793 12.3831 4.44414 12.204 4.57954 12.0606L4.58151 12.0586C4.72453 11.9155 4.90437 11.8447 5.11367 11.8447H8.79482C9.00363 11.8447 9.18092 11.9153 9.3177 12.0596C9.46006 12.2024 9.53057 12.3819 9.53057 12.5907C9.53057 12.7887 9.45812 12.9609 9.31671 13.1024C9.17936 13.2397 9.00235 13.306 8.79482 13.306H5.11367C4.90609 13.306 4.72695 13.2397 4.58358 13.1043L4.57954 13.1003ZM1.09476 0.851519C1.65317 0.285946 2.47955 0.0117188 3.55508 0.0117188H14.4447C15.52 0.0117188 16.3433 0.28583 16.895 0.851748C17.4529 1.41698 17.7234 2.24966 17.7234 3.33145V18.8866C17.7234 19.975 17.4531 20.8082 16.8945 21.3668C16.3427 21.9256 15.5196 22.1961 14.4447 22.1961H3.55508C2.47988 22.1961 1.65367 21.9255 1.09521 21.367C0.543652 20.8083 0.276367 19.9747 0.276367 18.8866V3.33145C0.276367 2.24984 0.543796 1.41679 1.09476 0.851519ZM15.5624 20.0351C15.2958 20.3085 14.8959 20.4452 14.3627 20.4452H3.63711C3.10391 20.4452 2.70059 20.3085 2.42715 20.0351L2.49875 19.9652L2.49786 19.9643L2.42715 20.0351C2.16055 19.7616 2.02725 19.3686 2.02725 18.8559V3.36221C2.02725 2.84951 2.16055 2.45645 2.42715 2.18301C2.70059 1.90273 3.10391 1.7626 3.63711 1.7626H14.3627C14.8959 1.7626 15.2958 1.90273 15.5624 2.18301C15.8358 2.45645 15.9726 2.84951 15.9726 3.36221V18.8559C15.9726 19.3686 15.8358 19.7616 15.5624 20.0351Z" fill="#3A3A3A"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,14 @@
<svg
width="24"
height="25"
viewBox="0 0 24 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20.709 15.8262C21.202 15.8262 21.5875 15.9577 21.8574 16.2275L21.9521 16.333C22.1583 16.592 22.2588 16.939 22.2588 17.3682V22.5098C22.2588 22.9389 22.1583 23.286 21.9521 23.5449L21.8574 23.6504C21.5875 23.9202 21.202 24.0517 20.709 24.0518H15.584C15.1523 24.0518 14.8024 23.9513 14.541 23.7451L14.4346 23.6504C14.1647 23.3833 14.0332 23 14.0332 22.5098V17.3682C14.0332 16.8779 14.1646 16.4947 14.4346 16.2275L14.541 16.1328C14.8024 15.9267 15.1523 15.8262 15.584 15.8262H20.709ZM17.4443 0.961914C18.5273 0.961914 19.3662 1.23768 19.9307 1.81641L20.0342 1.92773C20.5341 2.50088 20.7734 3.30971 20.7734 4.33105V13.6318C20.7734 14.1429 20.3587 14.5576 19.8477 14.5576C19.3367 14.5574 18.9229 14.1428 18.9229 13.6318V4.3623C18.9229 3.85862 18.7884 3.48005 18.5273 3.21875L18.5264 3.21777C18.2723 2.95072 17.8871 2.8125 17.3623 2.8125H6.63672C6.17751 2.81256 5.82275 2.91826 5.56641 3.12402L5.46289 3.21777C5.20816 3.47904 5.07715 3.85807 5.07715 4.3623V19.8555C5.07715 20.336 5.19802 20.7014 5.42969 20.9609L5.46289 20.9287L5.49805 20.8936L5.5332 20.9297H5.53418L5.56934 20.9658L5.5332 21.001L5.5 21.0332C5.76013 21.2713 6.13568 21.3954 6.63672 21.3955H11.625C12.1358 21.3957 12.5496 21.8095 12.5498 22.3203C12.5498 22.8313 12.1359 23.2459 11.625 23.2461H6.55469C5.53947 23.246 4.7368 23.0064 4.16992 22.5059L4.05957 22.4023C3.49544 21.8309 3.22658 20.9822 3.22656 19.8867V4.33105C3.22663 3.24207 3.49545 2.39422 4.05859 1.81641H4.05957L4.16895 1.71191C4.73581 1.20479 5.53881 0.961976 6.55469 0.961914H17.4443ZM17.3682 18.1484C17.2625 18.1485 17.1758 18.1703 17.1055 18.21L17.04 18.2559C16.9619 18.3244 16.9209 18.4175 16.9209 18.541C16.9209 18.6617 16.9604 18.7537 17.0361 18.8223L17.1006 18.8682C17.171 18.908 17.2583 18.9287 17.3643 18.9287H18.0039L18.5303 18.8613L18.6934 18.8398L18.5703 18.9482L17.9668 19.4795L16.5449 20.9004V20.9014C16.4462 20.9975 16.3985 21.1065 16.3984 21.2305C16.3984 21.3719 16.4428 21.4799 16.5273 21.5596H16.5264C16.6159 21.6414 16.723 21.6825 16.8496 21.6826C16.9174 21.6826 16.981 21.6706 17.04 21.6475L17.125 21.6025C17.1525 21.5839 17.1799 21.5613 17.2061 21.5352L18.6162 20.125L19.1416 19.5273L19.2451 19.4092L19.2285 19.5664L19.1689 20.1182V20.7168C19.169 20.8574 19.2064 20.9646 19.2754 21.0439L19.3311 21.0947C19.392 21.1378 19.4683 21.1592 19.5615 21.1592C19.6851 21.1591 19.7763 21.1186 19.8418 21.041H19.8428L19.8887 20.9756C19.9283 20.9057 19.9492 20.8201 19.9492 20.7168V18.6855C19.9492 18.5477 19.9209 18.4395 19.8682 18.3574L19.8076 18.2842C19.7144 18.196 19.5832 18.1484 19.4082 18.1484H17.3682ZM11.7949 12.7949C12.0155 12.7949 12.2056 12.87 12.3525 13.0244H12.3535C12.5057 13.1772 12.5811 13.3698 12.5811 13.5908C12.581 13.7495 12.5367 13.8931 12.4512 14.0186L12.3525 14.1377C12.2046 14.2856 12.0139 14.3564 11.7949 14.3564H8.11328C7.89437 14.3564 7.70227 14.2855 7.54883 14.1406L7.54297 14.1348C7.40022 13.9836 7.32815 13.7995 7.32812 13.5908C7.32812 13.3718 7.39806 13.1799 7.54297 13.0264L7.5459 13.0234L7.60547 12.9697C7.74785 12.8528 7.91945 12.795 8.11328 12.7949H11.7949ZM15.8965 9.21582C16.1111 9.21584 16.2969 9.29151 16.4424 9.44336L16.4961 9.5C16.6134 9.63628 16.6718 9.80257 16.6719 9.99121C16.6719 10.2068 16.5949 10.3944 16.4443 10.5459L16.4453 10.5469C16.2994 10.7008 16.1126 10.7773 15.8965 10.7773H8.11328C7.89178 10.7773 7.69883 10.7018 7.5459 10.5488V10.5479L7.54395 10.5459H7.54297C7.39907 10.3935 7.32812 10.2051 7.32812 9.99121C7.32821 9.77745 7.39921 9.59103 7.5459 9.44434L7.60547 9.39062C7.74779 9.27383 7.91955 9.2159 8.11328 9.21582H15.8965ZM15.8965 5.6377C16.1112 5.63772 16.2968 5.71321 16.4424 5.86523L16.4961 5.92188C16.6133 6.0582 16.6719 6.22441 16.6719 6.41309C16.6719 6.62878 16.595 6.81624 16.4443 6.96777L16.4453 6.96875C16.2994 7.12253 16.1125 7.19822 15.8965 7.19824H8.11328C7.89183 7.19815 7.69881 7.12357 7.5459 6.9707V6.96973L7.54297 6.9668C7.39925 6.81448 7.32815 6.62682 7.32812 6.41309C7.32812 6.19928 7.39928 6.01295 7.5459 5.86621C7.69883 5.71328 7.89178 5.63778 8.11328 5.6377H15.8965Z"
fill="#3A3A3A"
stroke="#3A3A3A"
stroke-width="0.1"
/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,30 @@
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M18.4661 8.93913C17.8524 8.93913 17.2752 8.82368 16.7344 8.59277C16.1997 8.35579 15.7257 8.03375 15.3125 7.62663C14.9054 7.21343 14.5833 6.73644 14.3464 6.19564C14.1155 5.65484 14 5.08062 14 4.47298C14 3.65267 14.2005 2.90527 14.6016 2.23079C15.0087 1.55632 15.5495 1.01855 16.224 0.617513C16.8984 0.210395 17.6458 0.00683594 18.4661 0.00683594C19.2804 0.00683594 20.0247 0.210395 20.6992 0.617513C21.3798 1.01855 21.9206 1.55632 22.3216 2.23079C22.7227 2.90527 22.9232 3.65267 22.9232 4.47298C22.9232 5.08062 22.8047 5.65484 22.5677 6.19564C22.3368 6.73036 22.0148 7.20432 21.6016 7.61751C21.1944 8.03071 20.7205 8.35579 20.1797 8.59277C19.6389 8.82368 19.0677 8.93913 18.4661 8.93913ZM18.4753 7.31673C18.6576 7.31673 18.8034 7.26204 18.9128 7.15267C19.0282 7.03722 19.0859 6.88835 19.0859 6.70606V5.08366H20.6992C20.8815 5.08366 21.0273 5.02897 21.1367 4.9196C21.2522 4.80415 21.3099 4.65527 21.3099 4.47298C21.3099 4.28461 21.2522 4.13574 21.1367 4.02637C21.0273 3.91699 20.8815 3.8623 20.6992 3.8623H19.0859V2.24902C19.0859 2.06673 19.0282 1.9209 18.9128 1.81152C18.8034 1.69607 18.6576 1.63835 18.4753 1.63835C18.2869 1.63835 18.135 1.69607 18.0195 1.81152C17.9102 1.9209 17.8555 2.06673 17.8555 2.24902V3.8623H16.2422C16.0599 3.8623 15.911 3.91699 15.7956 4.02637C15.6862 4.13574 15.6315 4.28461 15.6315 4.47298C15.6315 4.65527 15.6862 4.80415 15.7956 4.9196C15.911 5.02897 16.0599 5.08366 16.2422 5.08366H17.8555V6.70606C17.8555 6.88835 17.9102 7.03722 18.0195 7.15267C18.135 7.26204 18.2869 7.31673 18.4753 7.31673Z"
fill="#3A3A3A"
/>
<path
d="M20.7234 19.8862V9.53888C20.1803 9.77921 19.591 9.93419 18.9726 9.98682V19.8555C18.9726 20.3682 18.8358 20.7612 18.5624 21.0347C18.2958 21.3081 17.8959 21.4448 17.3627 21.4448H6.63711C6.10391 21.4448 5.70059 21.3081 5.42715 21.0347L5.49875 20.9649L5.49786 20.964L5.42715 21.0347C5.16055 20.7612 5.02725 20.3682 5.02725 19.8555V4.36181C5.02725 3.84911 5.16055 3.45605 5.42715 3.18261C5.70059 2.90234 6.10391 2.7622 6.63711 2.7622H13.2825C13.4981 2.11701 13.8301 1.52509 14.2535 1.01132H6.55508C5.47955 1.01132 4.65317 1.28555 4.09476 1.85112C3.5438 2.41639 3.27637 3.24944 3.27637 4.33105V19.8862C3.27637 20.9743 3.54365 21.8079 4.09521 22.3666C4.65367 22.9251 5.47988 23.1957 6.55508 23.1957H17.4447C18.5196 23.1957 19.3427 22.9252 19.8945 22.3664C20.4531 21.8078 20.7234 20.9746 20.7234 19.8862Z"
fill="#3A3A3A"
/>
<path
d="M13.6747 7.14833C13.4266 6.69614 13.2402 6.20528 13.1269 5.6871H8.11367C7.90437 5.6871 7.72456 5.75794 7.58154 5.90096C7.44495 6.03755 7.37793 6.21072 7.37793 6.41259C7.37793 6.61445 7.44486 6.78981 7.57954 6.93243L7.58154 6.93447C7.72456 7.0775 7.90437 7.14833 8.11367 7.14833H13.6747Z"
fill="#3A3A3A"
/>
<path
d="M16.5378 9.64649C16.2607 9.54063 15.9943 9.41301 15.7408 9.26572H8.11367C7.90437 9.26572 7.72456 9.33655 7.58154 9.47958C7.44495 9.61616 7.37793 9.78933 7.37793 9.99121C7.37793 10.1931 7.44486 10.3684 7.57954 10.511L7.58154 10.5131C7.72456 10.6561 7.90437 10.7269 8.11367 10.7269H15.8964C16.0996 10.7269 16.273 10.6555 16.409 10.5121C16.5506 10.37 16.6219 10.1944 16.6219 9.99121C16.6219 9.86401 16.5939 9.74847 16.5378 9.64649Z"
fill="#3A3A3A"
/>
<path
d="M7.57954 14.0999C7.44561 13.9581 7.37793 13.7865 7.37793 13.5903C7.37793 13.3827 7.44414 13.2036 7.57954 13.0602L7.58151 13.0582C7.72453 12.9151 7.90437 12.8443 8.11367 12.8443H11.7948C12.0036 12.8443 12.1809 12.9149 12.3177 13.0592C12.4601 13.2021 12.5306 13.3815 12.5306 13.5903C12.5306 13.7883 12.4581 13.9605 12.3167 14.102C12.1794 14.2393 12.0024 14.3056 11.7948 14.3056H8.11367C7.90609 14.3056 7.72695 14.2393 7.58358 14.1039L7.57954 14.0999Z"
fill="#3A3A3A"
/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 12 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3.40918 4.70117C3.28613 4.70117 3.18359 4.66016 3.10156 4.57812C3.02409 4.49609 2.98535 4.39583 2.98535 4.27734C2.98535 4.15885 3.02409 4.06087 3.10156 3.9834C3.18359 3.90137 3.28613 3.86035 3.40918 3.86035H8.59766C8.71615 3.86035 8.81413 3.90137 8.8916 3.9834C8.97363 4.06087 9.01465 4.15885 9.01465 4.27734C9.01465 4.39583 8.97363 4.49609 8.8916 4.57812C8.81413 4.66016 8.71615 4.70117 8.59766 4.70117H3.40918ZM3.40918 7.08691C3.28613 7.08691 3.18359 7.0459 3.10156 6.96387C3.02409 6.88184 2.98535 6.78158 2.98535 6.66309C2.98535 6.5446 3.02409 6.44661 3.10156 6.36914C3.18359 6.28711 3.28613 6.24609 3.40918 6.24609H8.59766C8.71615 6.24609 8.81413 6.28711 8.8916 6.36914C8.97363 6.44661 9.01465 6.5446 9.01465 6.66309C9.01465 6.78158 8.97363 6.88184 8.8916 6.96387C8.81413 7.0459 8.71615 7.08691 8.59766 7.08691H3.40918ZM3.40918 9.47266C3.28613 9.47266 3.18359 9.43392 3.10156 9.35645C3.02409 9.27441 2.98535 9.17643 2.98535 9.0625C2.98535 8.93945 3.02409 8.83691 3.10156 8.75488C3.18359 8.67285 3.28613 8.63184 3.40918 8.63184H5.86328C5.98633 8.63184 6.08659 8.67285 6.16406 8.75488C6.24609 8.83691 6.28711 8.93945 6.28711 9.0625C6.28711 9.17643 6.24609 9.27441 6.16406 9.35645C6.08659 9.43392 5.98633 9.47266 5.86328 9.47266H3.40918ZM0.250977 13.2598V2.88965C0.250977 2.17871 0.426432 1.64323 0.777344 1.2832C1.13281 0.923177 1.66374 0.743164 2.37012 0.743164H9.62988C10.3363 0.743164 10.8649 0.923177 11.2158 1.2832C11.5713 1.64323 11.749 2.17871 11.749 2.88965V13.2598C11.749 13.9753 11.5713 14.5107 11.2158 14.8662C10.8649 15.2217 10.3363 15.3994 9.62988 15.3994H2.37012C1.66374 15.3994 1.13281 15.2217 0.777344 14.8662C0.426432 14.5107 0.250977 13.9753 0.250977 13.2598ZM1.35156 13.2393C1.35156 13.5811 1.44043 13.8431 1.61816 14.0254C1.80046 14.2077 2.06934 14.2988 2.4248 14.2988H9.5752C9.93066 14.2988 10.1973 14.2077 10.375 14.0254C10.5573 13.8431 10.6484 13.5811 10.6484 13.2393V2.91016C10.6484 2.56836 10.5573 2.30632 10.375 2.12402C10.1973 1.93717 9.93066 1.84375 9.5752 1.84375H2.4248C2.06934 1.84375 1.80046 1.93717 1.61816 2.12402C1.44043 2.30632 1.35156 2.56836 1.35156 2.91016V13.2393Z"
fill="#8585F6"
/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -2,6 +2,7 @@ import { codeBlock } from '@blocknote/code-block';
import {
BlockNoteSchema,
defaultBlockSpecs,
defaultInlineContentSpecs,
withPageBreak,
} from '@blocknote/core';
import '@blocknote/core/fonts/inter.css';
@@ -18,8 +19,13 @@ import { Box, TextErrors } from '@/components';
import { Doc, useIsCollaborativeEditable } from '@/docs/doc-management';
import { useAuth } from '@/features/auth';
import { useHeadings, useUploadFile, useUploadStatus } from '../hook/';
import useSaveDoc from '../hook/useSaveDoc';
import {
useHeadings,
useSaveDoc,
useShortcuts,
useUploadFile,
useUploadStatus,
} from '../hook';
import { useEditorStore } from '../stores';
import { cssEditor } from '../styles';
import { DocsBlockNoteEditor } from '../types';
@@ -28,6 +34,10 @@ import { randomColor } from '../utils';
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
import { CalloutBlock, DividerBlock } from './custom-blocks';
import {
InterlinkingLinkInlineContent,
InterlinkingSearchInlineContent,
} from './custom-inline-content';
import XLMultiColumn from './xl-multi-column';
const multiColumnDropCursor = XLMultiColumn?.multiColumnDropCursor;
@@ -41,6 +51,11 @@ const baseBlockNoteSchema = withPageBreak(
callout: CalloutBlock,
divider: DividerBlock,
},
inlineContentSpecs: {
...defaultInlineContentSpecs,
interlinkingSearchInline: InterlinkingSearchInlineContent,
interlinkingLinkInline: InterlinkingLinkInlineContent,
},
}),
);
@@ -143,6 +158,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
);
useHeadings(editor);
useShortcuts(editor);
useUploadStatus(editor);
useEffect(() => {

View File

@@ -9,28 +9,49 @@ import {
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { DocsBlockSchema } from '../types';
import {
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema,
} from '../types';
import {
getCalloutReactSlashMenuItems,
getDividerReactSlashMenuItems,
} from './custom-blocks';
import { useGetInterlinkingMenuItems } from './custom-inline-content';
import XLMultiColumn from './xl-multi-column';
const getMultiColumnSlashMenuItems =
XLMultiColumn?.getMultiColumnSlashMenuItems;
export const BlockNoteSuggestionMenu = () => {
const editor = useBlockNoteEditor<DocsBlockSchema>();
const editor = useBlockNoteEditor<
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema
>();
const { t } = useTranslation();
const basicBlocksName = useDictionary().slash_menu.page_break.group;
const getInterlinkingMenuItems = useGetInterlinkingMenuItems();
const getSlashMenuItems = useMemo(() => {
// We insert it after the "Code Block" item to have the interlinking block displayed after the basic blocks
const defaultMenu = getDefaultReactSlashMenuItems(editor);
const index = defaultMenu.findIndex(
(item) => item.aliases?.includes('code') && item.aliases?.includes('pre'),
);
const newSlashMenuItems = [
...defaultMenu.slice(0, index + 1),
...getInterlinkingMenuItems(editor, t),
...defaultMenu.slice(index + 1),
];
return async (query: string) =>
Promise.resolve(
filterSuggestionItems(
combineByGroup(
getDefaultReactSlashMenuItems(editor),
newSlashMenuItems,
getCalloutReactSlashMenuItems(editor, t, basicBlocksName),
getMultiColumnSlashMenuItems?.(editor) || [],
getPageBreakReactSlashMenuItems(editor),
@@ -39,7 +60,7 @@ export const BlockNoteSuggestionMenu = () => {
query,
),
);
}, [basicBlocksName, editor, t]);
}, [basicBlocksName, editor, getInterlinkingMenuItems, t]);
return (
<SuggestionMenuController

View File

@@ -0,0 +1,80 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { createReactInlineContentSpec } from '@blocknote/react';
import { useEffect } from 'react';
import { css } from 'styled-components';
import { StyledLink, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import SelectedPageIcon from '@/docs/doc-editor/assets/doc-selected.svg';
import { useDoc } from '@/docs/doc-management';
export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
{
type: 'interlinkingLinkInline',
propSchema: {
url: {
default: '',
},
docId: {
default: '',
},
title: {
default: '',
},
},
content: 'none',
},
{
render: ({ inlineContent, updateInlineContent }) => {
const { data: doc } = useDoc({ id: inlineContent.props.docId });
useEffect(() => {
if (doc?.title && doc.title !== inlineContent.props.title) {
updateInlineContent({
type: 'interlinkingLinkInline',
props: {
...inlineContent.props,
title: doc.title,
},
});
}
}, [inlineContent.props, doc?.title, updateInlineContent]);
return <LinkSelected {...inlineContent.props} />;
},
},
);
interface LinkSelectedProps {
url: string;
title: string;
}
const LinkSelected = ({ url, title }: LinkSelectedProps) => {
const { colorsTokens } = useCunninghamTheme();
return (
<StyledLink
href={url}
draggable="false"
$css={css`
display: inline;
padding: 0.1rem 0.4rem;
border-radius: 4px;
& svg {
position: relative;
top: 2px;
margin-right: 0.2rem;
}
&:hover {
background-color: ${colorsTokens['greyscale-100']};
}
transition: background-color 0.2s ease-in-out;
`}
>
<SelectedPageIcon width={11.5} />
<Text $weight="500" spellCheck="false" $size="16px" $display="inline">
{title}
</Text>
</StyledLink>
);
};

View File

@@ -0,0 +1,86 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { createReactInlineContentSpec } from '@blocknote/react';
import { TFunction } from 'i18next';
import { DocsBlockNoteEditor } from '@/docs/doc-editor';
import LinkPageIcon from '@/docs/doc-editor/assets/doc-link.svg';
import AddPageIcon from '@/docs/doc-editor/assets/doc-plus.svg';
import { useCreateChildDocTree, useDocStore } from '@/docs/doc-management';
import { SearchPage } from './SearchPage';
export const InterlinkingSearchInlineContent = createReactInlineContentSpec(
{
type: 'interlinkingSearchInline',
propSchema: {
trigger: {
default: '/',
values: ['/', '@'],
},
disabled: {
default: false,
values: [true, false],
},
},
content: 'styled',
},
{
render: (props) => {
if (props.inlineContent.props.disabled) {
return null;
}
return (
<SearchPage
{...props}
trigger={props.inlineContent.props.trigger}
contentRef={props.contentRef}
/>
);
},
},
);
export const getInterlinkinghMenuItems = (
editor: DocsBlockNoteEditor,
t: TFunction<'translation', undefined>,
group: string,
createPage: () => void,
) => [
{
title: t('Link a doc'),
onItemClick: () => {
editor.insertInlineContent([
{
type: 'interlinkingSearchInline',
props: {
disabled: false,
trigger: '/',
},
},
]);
},
aliases: ['interlinking', 'link', 'anchor', 'a'],
group,
icon: <LinkPageIcon />,
subtext: t('Link this doc to another doc'),
},
{
title: t('New sub-doc'),
onItemClick: createPage,
aliases: ['new sub-doc'],
group,
icon: <AddPageIcon />,
subtext: t('Create a new sub-doc'),
},
];
export const useGetInterlinkingMenuItems = () => {
const { currentDoc } = useDocStore();
const createChildDoc = useCreateChildDocTree(currentDoc?.id);
return (
editor: DocsBlockNoteEditor,
t: TFunction<'translation', undefined>,
) => getInterlinkinghMenuItems(editor, t, t('Links'), createChildDoc);
};

View File

@@ -0,0 +1,319 @@
/* eslint-disable react-hooks/rules-of-hooks */
import {
PartialCustomInlineContentFromConfig,
StyleSchema,
} from '@blocknote/core';
import { useBlockNoteEditor } from '@blocknote/react';
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import {
Box,
Card,
Icon,
QuickSearch,
QuickSearchGroup,
QuickSearchItemContent,
Text,
} from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import {
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema,
} from '@/docs/doc-editor';
import FoundPageIcon from '@/docs/doc-editor/assets/doc-found.svg';
import AddPageIcon from '@/docs/doc-editor/assets/doc-plus.svg';
import {
useCreateChildDocTree,
useDocStore,
useTrans,
} from '@/docs/doc-management';
import { DocSearchSubPageContent, DocSearchTarget } from '@/docs/doc-search';
import { useResponsiveStore } from '@/stores';
const inputStyle = css`
background-color: var(--c--theme--colors--greyscale-100);
border: none;
outline: none;
color: var(--c--theme--colors--greyscale-700);
font-size: 16px;
width: 100%;
font-family: 'Inter';
`;
type SearchPageProps = {
trigger: string;
updateInlineContent: (
update: PartialCustomInlineContentFromConfig<
{
type: string;
propSchema: {
disabled: {
default: boolean;
};
trigger: {
default: string;
};
};
content: 'styled';
},
StyleSchema
>,
) => void;
contentRef: (node: HTMLElement | null) => void;
};
export const SearchPage = ({
contentRef,
trigger,
updateInlineContent,
}: SearchPageProps) => {
const { colorsTokens } = useCunninghamTheme();
const editor = useBlockNoteEditor<
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema
>();
const { t } = useTranslation();
const { currentDoc } = useDocStore();
const createChildDoc = useCreateChildDocTree(currentDoc?.id);
const inputRef = useRef<HTMLInputElement>(null);
const [search, setSearch] = useState('');
const { isDesktop } = useResponsiveStore();
const { untitledDocument } = useTrans();
/**
* createReactInlineContentSpec add automatically the focus after
* the inline content, so we need to set the focus on the input
* after the component is mounted.
*/
useEffect(() => {
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, 100);
}, [inputRef]);
return (
<Box as="span" $position="relative">
<Box
as="span"
className="inline-content"
$background={colorsTokens['greyscale-100']}
$color="var(--c--theme--colors--greyscale-700)"
$direction="row"
$radius="3px"
$padding="1px"
$display="inline-flex"
tabIndex={-1} // Ensure the span is focusable
>
{' '}
{trigger}
<Box
as="input"
$padding={{ left: '3px' }}
$css={inputStyle}
ref={inputRef}
$display="inline-flex"
onInput={(e) => {
const value = (e.target as HTMLInputElement).value;
setSearch(value);
}}
onKeyDown={(e) => {
if (
(e.key === 'Backspace' && search.length === 0) ||
e.key === 'Escape'
) {
e.preventDefault();
updateInlineContent({
type: 'interlinkingSearchInline',
props: {
disabled: true,
trigger,
},
});
contentRef(null);
editor.focus();
editor.insertInlineContent(['']);
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
// Allow arrow keys to be handled by the command menu for navigation
const commandList = e.currentTarget
.closest('.inline-content')
?.nextElementSibling?.querySelector('[cmdk-list]');
// Create a synthetic keyboard event for the command menu
const syntheticEvent = new KeyboardEvent('keydown', {
key: e.key,
bubbles: true,
cancelable: true,
});
commandList?.dispatchEvent(syntheticEvent);
e.preventDefault();
} else if (e.key === 'Enter') {
// Handle Enter key to select the currently highlighted item
const selectedItem = e.currentTarget
.closest('.inline-content')
?.nextElementSibling?.querySelector(
'[cmdk-item][data-selected="true"]',
) as HTMLElement;
selectedItem?.click();
e.preventDefault();
}
}}
/>
</Box>
<Box
$minWidth={isDesktop ? '330px' : '220px'}
$width="fit-content"
$position="absolute"
$css={css`
top: 28px;
z-index: 1000;
& .quick-search-container [cmdk-root] {
border-radius: inherit;
}
`}
>
<QuickSearch showInput={false}>
<Card
$css={css`
box-shadow: 0 0 3px 0px var(--c--theme--colors--greyscale-200);
& > div {
margin-top: 0;
& [cmdk-group-heading] {
padding: 0.4rem;
margin: 0;
}
& [cmdk-group-items] .ml-b {
margin-left: 0rem;
padding: 0.5rem;
font-size: 14px;
display: block;
}
& [cmdk-item] {
border-radius: 0;
}
& .--docs--doc-search-item > div {
gap: 0.8rem;
}
}
`}
$margin={{ top: '0.5rem' }}
>
<DocSearchSubPageContent
search={search}
filters={{ target: DocSearchTarget.CURRENT }}
onSelect={(doc) => {
updateInlineContent({
type: 'interlinkingSearchInline',
props: {
disabled: true,
trigger,
},
});
editor.insertInlineContent([
{
type: 'interlinkingLinkInline',
props: {
url: `/docs/${doc.id}`,
docId: doc.id,
title: doc.title || untitledDocument,
},
},
]);
editor.focus();
}}
renderElement={(doc) => (
<QuickSearchItemContent
left={
<Box
$direction="row"
$gap="0.6rem"
$align="center"
$padding={{ vertical: '0.5rem', horizontal: '0.2rem' }}
$width="100%"
>
<FoundPageIcon />
<Text
$size="14px"
$color="var(--c--theme--colors--greyscale-1000)"
spellCheck="false"
>
{doc.title}
</Text>
</Box>
}
right={
<Icon
iconName="keyboard_return"
$variation="600"
spellCheck="false"
/>
}
/>
)}
/>
<QuickSearchGroup
group={{
groupName: '',
elements: [],
endActions: [
{
onSelect: createChildDoc,
content: (
<Box
$css={css`
border-top: 1px solid
var(--c--theme--colors--greyscale-200);
`}
$width="100%"
>
<Box
$direction="row"
$gap="0.4rem"
$align="center"
$padding={{
vertical: '0.5rem',
horizontal: '0.3rem',
}}
$css={css`
&:hover {
background-color: var(
--c--theme--colors--greyscale-100
);
}
`}
>
<AddPageIcon />
<Text
$size="14px"
$color="var(--c--theme--colors--greyscale-1000)"
contentEditable={false}
>
{t('New sub-doc')}
</Text>
</Box>
</Box>
),
},
],
}}
/>
</Card>
</QuickSearch>
</Box>
</Box>
);
};

View File

@@ -0,0 +1,2 @@
export * from './InterlinkingLinkInlineContent';
export * from './InterlinkingSearchInlineContent';

View File

@@ -0,0 +1 @@
export * from './Interlinking';

View File

@@ -5,7 +5,7 @@ import * as Y from 'yjs';
import { AppWrapper } from '@/tests/utils';
import useSaveDoc from '../useSaveDoc';
import { useSaveDoc } from '../useSaveDoc';
jest.mock('next/router', () => ({
useRouter: jest.fn(),

View File

@@ -1,3 +1,4 @@
export * from './useHeadings';
export * from './useSaveDoc';
export * from './useShortcuts';
export * from './useUploadFile';

View File

@@ -10,7 +10,7 @@ import { toBase64 } from '../utils';
const SAVE_INTERVAL = 60000;
const useSaveDoc = (
export const useSaveDoc = (
docId: string,
yDoc: Y.Doc,
canSave: boolean,
@@ -105,5 +105,3 @@ const useSaveDoc = (
};
}, [router.events, saveDoc]);
};
export default useSaveDoc;

View File

@@ -0,0 +1,39 @@
import { useEffect } from 'react';
import { DocsBlockNoteEditor } from '../types';
export const useShortcuts = (editor: DocsBlockNoteEditor) => {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === '@' && editor?.isFocused()) {
const selection = window.getSelection();
const previousChar =
selection?.anchorNode?.textContent?.charAt(
selection.anchorOffset - 1,
) || '';
if (![' ', ''].includes(previousChar)) {
return;
}
event.preventDefault();
editor.insertInlineContent([
{
type: 'interlinkingSearchInline',
props: {
disabled: false,
trigger: '@',
},
},
]);
}
};
// Attach the event listener to the document instead of the window
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [editor]);
};

View File

@@ -88,7 +88,7 @@ export const cssEditor = (readonly: boolean) => css`
font-size: 1.25rem;
}
a {
color: var(--c--theme--colors--greyscale-500);
color: var(--c--theme--colors--greyscale-600);
cursor: pointer;
}
.bn-block-group

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

View File

@@ -10,7 +10,7 @@ export const blockMappingParagraphPDF: DocsExporterPDF['mappings']['blockMapping
*/
if (Array.isArray(block.content)) {
block.content.forEach((content) => {
if (content.type === 'text' && !content.text) {
if (content.type === 'text' && 'text' in content && !content.text) {
content.text = ' ';
}
});

View File

@@ -8,11 +8,17 @@ export const blockMappingQuoteDocx: DocsExporterDocx['mappings']['blockMapping']
if (Array.isArray(block.content)) {
block.content.forEach((content) => {
if (content.type === 'text') {
content.styles = {
...content.styles,
italic: true,
textColor: 'gray',
};
if (
'styles' in content &&
typeof content.styles === 'object' &&
content.styles !== null
) {
content.styles = {
...content.styles,
italic: true,
textColor: 'gray',
};
}
}
});
}

View File

@@ -10,7 +10,8 @@ import {
useToastProvider,
} from '@openfun/cunningham-react';
import { DocumentProps, pdf } from '@react-pdf/renderer';
import { useMemo, useState } from 'react';
import i18next from 'i18next';
import { cloneElement, isValidElement, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -92,10 +93,15 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
const exporter = new PDFExporter(editor.schema, pdfDocsSchemaMappings, {
resolveFileUrl: async (url) => exportCorsResolveFileUrl(doc.id, url),
});
const pdfDocument = (await exporter.toReactPDFDocument(
const rawPdfDocument = (await exporter.toReactPDFDocument(
exportDocument,
)) as React.ReactElement<DocumentProps>;
// Inject language for screen reader support
const pdfDocument = isValidElement(rawPdfDocument)
? cloneElement(rawPdfDocument, { language: i18next.language })
: rawPdfDocument;
blobExport = await pdf(pdfDocument).toBlob();
} else {
const exporter = new DOCXExporter(editor.schema, docxDocsSchemaMappings, {

View File

@@ -0,0 +1,2 @@
export * from './interlinkingLinkPDF';
export * from './interlinkingLinkDocx';

View File

@@ -0,0 +1,16 @@
import { ExternalHyperlink, TextRun } from 'docx';
import { DocsExporterDocx } from '../types';
export const inlineContentMappingInterlinkingLinkDocx: DocsExporterDocx['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
(inline) => {
return new ExternalHyperlink({
children: [
new TextRun({
text: `📄${inline.props.title}`,
bold: true,
}),
],
link: window.location.origin + inline.props.url,
});
};

View File

@@ -0,0 +1,22 @@
/* eslint-disable jsx-a11y/alt-text */
import { Image, Link, Text } from '@react-pdf/renderer';
import DocSelectedIcon from '../assets/doc-selected.png';
import { DocsExporterPDF } from '../types';
export const inlineContentMappingInterlinkingLinkPDF: DocsExporterPDF['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
(inline) => {
return (
<Link
src={window.location.origin + inline.props.url}
style={{
textDecoration: 'none',
color: 'black',
}}
>
{' '}
<Image src={DocSelectedIcon.src} />{' '}
<Text>{inline.props.title}</Text>{' '}
</Link>
);
};

View File

@@ -1,4 +1,5 @@
import { docxDefaultSchemaMappings } from '@blocknote/xl-docx-exporter';
import { Paragraph } from 'docx';
import {
blockMappingCalloutDocx,
@@ -6,6 +7,7 @@ import {
blockMappingImageDocx,
blockMappingQuoteDocx,
} from './blocks-mapping';
import { inlineContentMappingInterlinkingLinkDocx } from './inline-content-mapping';
import { DocsExporterDocx } from './types';
export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
@@ -17,4 +19,9 @@ export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
quote: blockMappingQuoteDocx,
image: blockMappingImageDocx,
},
inlineContentMapping: {
...docxDefaultSchemaMappings.inlineContentMapping,
interlinkingSearchInline: () => new Paragraph(''),
interlinkingLinkInline: inlineContentMappingInterlinkingLinkDocx,
},
};

View File

@@ -9,6 +9,7 @@ import {
blockMappingQuotePDF,
blockMappingTablePDF,
} from './blocks-mapping';
import { inlineContentMappingInterlinkingLinkPDF } from './inline-content-mapping';
import { DocsExporterPDF } from './types';
export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = {
@@ -23,4 +24,9 @@ export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = {
quote: blockMappingQuotePDF,
table: blockMappingTablePDF,
},
inlineContentMapping: {
...pdfDefaultSchemaMappings.inlineContentMapping,
interlinkingSearchInline: () => <></>,
interlinkingLinkInline: inlineContentMappingInterlinkingLinkPDF,
},
};

View File

@@ -43,9 +43,9 @@ export type DocsExporterPDF = Exporter<
>;
export type DocsExporterDocx = Exporter<
NoInfer<DocsBlockSchema>,
NoInfer<DocsInlineContentSchema>,
NoInfer<DocsStyleSchema>,
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema,
Promise<Paragraph[] | Paragraph | Table> | Paragraph[] | Paragraph | Table,
ParagraphChild,
IRunPropertiesOptions,

View File

@@ -24,7 +24,6 @@ import {
useCreateFavoriteDoc,
useDeleteFavoriteDoc,
useDuplicateDoc,
useMaskDocOption,
} from '@/docs/doc-management';
import { DocShareModal } from '@/docs/doc-share';
import {
@@ -82,7 +81,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const makeFavoriteDoc = useCreateFavoriteDoc({
listInvalideQueries: [KEY_LIST_DOC, KEY_DOC],
});
const maskDocOption = useMaskDocOption(doc);
useEffect(() => {
if (selectHistoryModal.isOpen) {
@@ -128,7 +126,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
}
},
testId: `docs-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`,
showSeparator: true,
},
{
label: t('Version history'),
@@ -165,23 +162,17 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
canSave: doc.abilities.partial_update,
});
},
showSeparator: true,
},
{
label: t('Delete document'),
icon: 'delete',
disabled: !doc.abilities.destroy,
callback: () => {
setIsModalRemoveOpen(true);
},
},
];
const leaveDocOption: DropdownMenuOption = doc.abilities.destroy
? {
label: t('Delete document'),
icon: 'delete',
disabled: !doc.abilities.destroy,
callback: () => {
setIsModalRemoveOpen(true);
},
}
: maskDocOption;
options.push(leaveDocOption);
const copyCurrentEditorToClipboard = useCopyCurrentEditorToClipboard();
return (

View File

@@ -1,11 +1,11 @@
export * from './useCreateChildDoc';
export * from './useCreateDoc';
export * from './useCreateFavoriteDoc';
export * from './useDeleteFavoriteDoc';
export * from './useDoc';
export * from './useDocOptions';
export * from './useDocs';
export * from './useDuplicateDoc';
export * from './useMaskDoc';
export * from './useSubDocs';
export * from './useDuplicateDoc';
export * from './useUpdateDoc';
export * from './useUpdateDocLink';

Some files were not shown because too many files have changed in this diff Show More