Compare commits

...

51 Commits

Author SHA1 Message Date
Anthony LC
3cb7aeb7ec 💩(backend) add document content endpoint
Get the content of a document in markdown format.
Ex: http://localhost:8071/api/v1.0/documents/<ID>/content/
2025-06-03 12:12:19 +02:00
Anthony LC
23860065e1 env.example 2025-06-03 11:19:18 +02:00
Anthony LC
f459c56121 📝(mcp) add doc to use mcp with vscode 2025-05-30 17:48:16 +02:00
Quentin BEY
4a81e1526e 👷(hackdays) publish the MCP docker image
Publish the MCP Docker image on our registry.
2025-05-28 11:29:38 +02:00
Quentin BEY
abcd61cf2f 👷(hackdays) publish the docker image
Publish the Docker images to deploy on a dedicated instance for the
Hackdays.
2025-05-28 11:29:38 +02:00
Quentin BEY
c1a591fb4f 🧱(mcp) add server deployment
Provide the helm chart declaration to deploy the MCP server.
2025-05-28 11:29:38 +02:00
Quentin BEY
83d8478b5d 💩(mcp) add a local MCP server configuration
This provides a way to start a local MCP server:
 - provided a user token, the MCP can create document
 - can be run locally and work with cursor or mcphost
2025-05-28 11:29:38 +02:00
Quentin BEY
6bd136c76e 💩(user-tokens) add back & front for Token auth
This provides:
 - a frontend to allow user to create/delete User Token
 - the authentication process to allow any API to be called when
   authenticating with a User Token.
2025-05-26 14:51:08 +02:00
Quentin BEY
e929fcc682 💩(resource-server) open all APIs to RS
This provides a base configuration to allow to access all
API via OIDC resource server authentication.
2025-05-26 11:39:43 +02:00
Quentin BEY
fa819bc1ff 🐛(auth) allow several auth backend on m2m API
The previous `ServerToServerAuthentication` was raising authentication
failed error if anything is wrong (the header, the token) which prevents
any possibility to have several authentication backends.
2025-05-26 11:39:43 +02:00
Quentin BEY
43e529da2a 🔒️(oidc) disable OIDC authentication on API
Our authentication flow uses the Django authentication which creates a
session for the User. Then the session is used to make API calls,
therefore we don't need to accept OIDC tokens directly on the API.

Accepting the OIDC token on the API can allow to bypass the "resource
server mode" which allows to restrict provided information according to
the Service Provider which makes the request.
2025-05-26 11:39:43 +02:00
renovate[bot]
cde64ed80a ⬆️(dependencies) update js dependencies 2025-05-26 06:39:40 +00:00
renovate[bot]
cfd88d0469 ⬆️(dependencies) update python dependencies 2025-05-26 01:55:36 +00:00
virgile-dev
5e45fec296 📝(doc) fix path to env doc on readme (#1007)
The path lead to a 404

Signed-off-by: virgile-dev <virgile.deville@beta.gouv.fr>
2025-05-25 17:01:29 +00:00
Anthony LC
393e7a06e2 🔖(minor) release 3.3.0
Added:
- (backend) add endpoint checking media status
- (backend) allow setting session cookie age via env var
- (backend) allow theme customnization using a
configuration file
- (frontend) Add a custom callout block to the editor
- 🚩(frontend) version MIT only
- (backend) integrate maleware_detection from django-lasuite
- 🏗️(frontend) Footer configurable
- 🩺(CI) add lint spell mistakes
- (frontend) create generic theme
- 🛂(frontend) block edition to not connected users
- 🚸(frontend) Let loader during upload analyze
- 🚩(frontend) feature flag on blocking edition

Changed:
- 📝(frontend) Update documentation
- (frontend) Improve tests coverage
- ⬆️(docker) upgrade backend image to python 3.13
- ⬆️(docker) upgrade node images to alpine 3.21

Fixed:
- 🐛(y-provider) increase JSON size limits for
transcription conversion

Removed:
- 🔥(back) remove footer endpoint
2025-05-23 11:41:24 +02:00
AntoLC
f1af87baf8 🌐(i18n) update translated strings
Update translated files with new translations
2025-05-23 11:41:24 +02:00
Anthony LC
f851ef2d85 ⬆️(dependencies) bump blocknote to 0.30.1
A bunch of fixes are in this pacth release.
Better to update now before we release to 3.3.0.
2025-05-23 11:08:07 +02:00
Anthony LC
252ab6a586 ✏️(frontend) change antivirus fail sentence
The message was not accurate when the analizer failed.
We improved the message to be more accurate.
2025-05-23 11:08:07 +02:00
Anthony LC
cf2a02c8de 🚩(frontend) feature flag on blocking edition
If users were not connected to the collaboration
server, they were not be able to edit documents.

We decided to add a feature flag on this feature
as it can be quite restrictive.
We can now enable or disable this feature at runtime
thanks to the env variable
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY".
2025-05-23 11:08:07 +02:00
Anthony LC
d87a2ed4eb 🔥(helm) remove useless footer config
Remove:
- FRONTEND_FOOTER_FEATURE_ENABLED
- FRONTEND_FOOTER_URL
2025-05-22 15:27:39 +02:00
Anthony LC
c9d053d1c0 💄(frontend) add generic favicon
The favicons were still with the dsfr color.
We added the generic favicon in the assets folder.
The favicon can be a url loaded from the theme,
so when Drive will be running, we will be able
to store the dsfr favicons there, and remove them
from the repo.
2025-05-22 15:27:38 +02:00
Anthony LC
b5f0f06ea3 💄(frontend) desaturate images system for generic theme
We want to desaturate the images system in the
generic theme to make them less colorful and more
in line with the overall theme.
We added a special class to the images
that need to be desaturated. Other property
then desaturated can be apply depending on the theme.
2025-05-22 15:27:38 +02:00
Anthony LC
36b0ff9f63 (frontend) create generic theme
By default Docs will not be on the dsfr theme but
on the generic theme. La Gaufre is part of the dsfr
theme and is removed from the generic theme.
Same for the "beta" keyword and the "proconnect"
buttons.
2025-05-22 15:27:38 +02:00
Anthony LC
7a383957a7 🔥(frontend) remove legal pages
Legal pages are not needed anymore in the application.
In the dsfr instances, the legal pages will be
displayed on a Docs pages.
We let the users of Docs managing the legal pages
on their own instances.
2025-05-22 14:07:41 +02:00
Anthony LC
b5630359ee 🏗️(frontend) Footer configurable
To have different footer per instance the
content of the footer is now configurable
from the theme customization file.

See THEME_CUSTOMIZATION_FILE_PATH env var.
2025-05-22 14:07:41 +02:00
Anthony LC
310154815b ♻️(e2e) improve config testcases
Improve config testcases:
- let THEME_CUSTOMIZATION_FILE_PATH to be set to
check the default value
- add helper function overrideConfig
2025-05-22 14:07:41 +02:00
Anthony LC
2733785016 🚨(linter) add ignore pattern on no-unused-vars rule
The rule @typescript-eslint/no-unused-vars didn't
have a ignore pattern. A ignore pattern can be
usefull in some cases.
2025-05-22 14:07:41 +02:00
Manuel Raynaud
99ba414d88 🔧(back) add docs.security to logging settings
In the malware_detection callback we are using a different logger named
docs.security. We want to configure a logger in the logging settings
handling it.
2025-05-22 13:53:27 +02:00
Manuel Raynaud
41631b5b70 ⬆️(backend) upgrade django-lasuite to version 0.0.9
We need version 0.0.9 to reduce the time to have a JCOP analysis result.
2025-05-22 13:53:27 +02:00
Anthony LC
6ca654bf1a 🚸(frontend) let loader until resource ready
The backend can analyze the upload file, this take
time, so we need to show a loader
until the backend finish the analysis.
2025-05-22 13:53:27 +02:00
Manuel Raynaud
074585337b ♻️(back) return the media-check url on the attachment_upload response
We want to have the media-check url returned on the attachment-upload
response instead of the media url directly. The front will know the
endpoint to use to check the media status.
2025-05-22 13:39:44 +02:00
Manuel Raynaud
f1b398e1ae (back) add endpoint checking media status
With the usage of a malware detection system, we need a way to know the
file status. The front will use it to display a loader while the analyse
is not ended.
2025-05-22 13:39:44 +02:00
Jacques ROUSSEL
d1f73f18cd 🔒️(front) improve docker image security
Cyberwatch reported security issues with the frontend Docker image.
2025-05-22 11:16:57 +02:00
lebaudantoine
3f2d84bf62 🐛(y-provider) increase JSON size limits for transcription conversion
Problem:
- Default Express JSON parser limit (100kb) is insufficient for larger
 transcription files
- 2-hour audio transcriptions slightly exceed the 100kb limit, causing request
 failures

Solution:
- Implemented custom middleware to apply different JSON parser configurations
 based on route
- Applied 500kb limit specifically for transcription conversion endpoints
- Maintained default limits for all other routes to preserve security

Technical notes:
- Could not find a built-in Express solution to specify parser config per route
- Custom middleware conditionally applies the appropriate parser configuration
2025-05-21 15:31:49 +02:00
lebaudantoine
7b9c362d38 🐛(tilt) update certificate path for Python 3.13 upgrade
Fix certificate directory reference that still pointed to Python 3.12 folder
after upgrading to Python 3.13. Resolves certificate verification errors in
tilt stack caused by incorrect certificate location.
2025-05-21 12:53:41 +02:00
virgile-dev
bf999979d2 📝(doc) update xl packages warning (#985)
So that people know how to use the PUBLISH_AS_MIT variable

Signed-off-by: virgile-dev <virgile.deville@beta.gouv.fr>
2025-05-20 21:10:54 +02:00
renovate[bot]
09d3ff3754 ⬆️(dependencies) update python dependencies 2025-05-19 12:21:04 +00:00
Samuel Paccoud - DINUM
6e5d005dee (backend) allow setting session cookie age via env var
We want to be able to increase the duration of the cookie session
by setting an environment variable.
2025-05-19 13:57:30 +02:00
Anthony LC
6377c8fcca ✈️(frontend) allow editing when offline
When the user is offline, we allow editing the
document in the editor.
Their is not a reliable way to know if the user is
offline or online except by doing a network request
and checking if an error is thrown or not.
To do so, we created the OfflinePlugin inherited
from the WorkboxPlugin.
It will inform us if the user is offline or online.
We then dispatch the information to our application
thanks to the useOffline hook.
2025-05-19 12:36:32 +02:00
Anthony LC
3c8cacc048 🛂(frontend) block edition to not connected users
If an editor is working on a shared document but
is not connected to the collaborative server
we are now blocking the edition.
It is to avoid none connected users to
overwrite the document with connected
users.
2025-05-19 12:36:31 +02:00
virgile-dev
598fb4fa27 📝(doc) update issue templates (#976)
Mention of Impress instead of Docs is confusing. Also added some
automatic labelling.

Signed-off-by: virgile-dev <virgile.deville@beta.gouv.fr>
2025-05-19 09:14:00 +00:00
Anthony LC
51618ad081 📌(dependencies) add hocuspocus to renovate ignored list
hocuspocus > 3.0.0 brings breaking changes.
Let's add it in the renovate.json ignored list
until we decide to upgrade it.
2025-05-19 09:19:37 +02:00
renovate[bot]
8109d5ba08 ⬆️(dependencies) update js dependencies 2025-05-19 09:17:33 +02:00
virgile-dev
e4d0179bbe 📝(doc) readme update (#974)
Added badges so people can assess the repo activity easily. 
Also update the paragraph about AGPL XL packages since this was merged :
https://github.com/suitenumerique/docs/pull/911

---------

Signed-off-by: virgile-dev <virgile.deville@beta.gouv.fr>
2025-05-16 17:48:57 +00:00
Manuel Raynaud
9d3dfb6de7 ⬆️(docker) upgrade node images to alpine 3.21
We need to upgrade our images to alpine 3.21 in order to fix a CVE
related to libxml2. We also upgrade node to version 24
2025-05-16 15:55:33 +02:00
Manuel Raynaud
0da042f887 ⬆️(docker) upgrade backend image to python 3.13
Python 3.13 is now stable, our libraries are compatible with it. We also
upgrade the alpine version used in order to fix CVE related to libxml2
2025-05-16 15:55:32 +02:00
Anthony LC
6cd0cd0689 ⬆️(dependencies) gouvfr-lasuite/ui-kit to 0.6.0
Upgrade @gouvfr-lasuite/ui-kit from 0.5.0 to 0.6.0.
Some properties have been removed in 0.5.0, which
causes the design of the app to be broken.
Version 0.6.0 has as well some breaking changes,
about the "logo" properties that are not available anymore.
We fix them in this commit.
2025-05-16 12:35:16 +02:00
Anthony LC
10b088599c 🐛(frontend) fix svg export
Last upgrade of Blocknote to 0.30.0 broke the SVG
export. The previewWidth can be undefined, which causes the
export to fail. This commit adds a fallback
width in case previewWidth is undefined.
2025-05-16 11:12:30 +02:00
Anthony LC
62d1bc6473 🐛(frontend) redirect to /home
The page '/login' was replaced with '/home',
but some users may still have the old URL in their
bookmarks, it can create a loop during the
authentication process.
We redirect the user to '/home' if they try to access
'/login' page, it will prevent edge cases.
2025-05-16 11:12:30 +02:00
Anthony LC
fc1d33268c ⬆️(dependencies) update js dependencies 2025-05-16 11:12:30 +02:00
virgile-dev
95833fa5ec 📝(documentation) add banner to readme (#970)
Implement suggestions made by @xibe in
[#848](https://github.com/suitenumerique/docs/pull/848) and
[#849](https://github.com/suitenumerique/docs/pull/849)

Signed-off-by: virgile-dev <virgile.deville@beta.gouv.fr>
2025-05-16 11:10:05 +02:00
182 changed files with 9102 additions and 3739 deletions

View File

@@ -1,7 +1,7 @@
---
name: 🐛 Bug Report
about: If something is not working as expected 🤔.
labels: ["bug", "triage"]
---
## Bug Report
@@ -18,8 +18,8 @@ A clear and concise description of what you expected to happen (or code).
3. And then the bug happens!
**Environment**
- Impress version:
- Platform:
- Docs version:
- Instance url:
**Possible Solution**
<!--- Only if you have suggestions on a fix for the bug -->

View File

@@ -1,7 +1,7 @@
---
name: ✨ Feature Request
about: I have a suggestion (and may want to build it 💪)!
labels: ["feature", "triage"]
---
## Feature Request
@@ -16,8 +16,8 @@ A clear and concise description of what you want to happen. Add any considered d
A clear and concise description of any alternative solutions or features you've considered.
**Discovery, Documentation, Adoption, Migration Strategy**
If you can, explain how users will be able to use this and possibly write out a version the docs (if applicable).
Maybe a screenshot or design?
If you can, explain how users will be able to use this and possibly write out some documentation (if applicable).
Maybe add a screenshot or design?
**Do you want to work on it through a Pull Request?**
<!-- Make sure to coordinate with us before you spend too much time working on an implementation! -->

View File

@@ -1,17 +1,13 @@
---
name: 🤗 Support Question
about: If you have a question 💬, or something was not clear from the docs!
labels: ["support", "triage"]
---
## Support request
**Checks before filing**
Please make sure you have read our [main Readme](https://github.com/suitenumerique/docs).
<!-- ^ Click "Preview" for a nicer view! ^
We primarily use GitHub as an issue tracker. If however you're encountering an issue not covered in the docs, we may be able to help! -->
---
Please make sure you have read our [main Readme](https://github.com/numerique-gouv/impress).
Also make sure it was not already answered in [an open or close issue](https://github.com/numerique-gouv/impress/issues).
Also make sure it was not already answered in [an open or close issue](https://github.com/suitenumerique/docs/issues?q=is%3Aissue%20state%3Aopen%20label%3Asupport).
If your question was not covered, and you feel like it should be, fire away! We'd love to improve our docs! 👌

View File

@@ -25,7 +25,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v3
with:
python-version: "3.12.6"
python-version: "3.13.3"
- name: Upgrade pip and setuptools
run: pip install --upgrade pip setuptools
- name: Install development dependencies

View File

@@ -5,13 +5,7 @@ on:
workflow_dispatch:
push:
branches:
- 'main'
tags:
- 'v*'
pull_request:
branches:
- 'main'
- 'ci/trivy-fails'
- 'do-not-merge/hackathon-2025'
env:
DOCKER_USER: 1001:127
@@ -31,7 +25,6 @@ jobs:
images: lasuite/impress-backend
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
-
name: Run trivy scan
@@ -43,10 +36,10 @@ jobs:
name: Build and push
uses: docker/build-push-action@v6
with:
push: true
context: .
target: backend-production
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -64,7 +57,6 @@ jobs:
images: lasuite/impress-frontend
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
-
name: Run trivy scan
@@ -76,13 +68,13 @@ jobs:
name: Build and push
uses: docker/build-push-action@v6
with:
push: true
context: .
file: ./src/frontend/Dockerfile
target: frontend-production
build-args: |
DOCKER_USER=${{ env.DOCKER_USER }}:-1000
PUBLISH_AS_MIT=false
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -100,7 +92,6 @@ jobs:
images: lasuite/impress-y-provider
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
-
name: Run trivy scan
@@ -112,11 +103,34 @@ jobs:
name: Build and push
uses: docker/build-push-action@v6
with:
push: true
context: .
file: ./src/frontend/servers/y-provider/Dockerfile
target: y-provider
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-and-push-mcp-server:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: lasuite/impress-mcp-server
- name: Login to DockerHub
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
context: ./src/mcp_server
file: ./src/mcp_server/Dockerfile
build-args: |
DOCKER_USER=${{ env.DOCKER_USER }}:-1000
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -125,7 +139,6 @@ jobs:
- build-and-push-frontend
- build-and-push-backend
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
steps:
- uses: numerique-gouv/action-argocd-webhook-notification@main
id: notify

View File

@@ -91,7 +91,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v3
with:
python-version: "3.12.6"
python-version: "3.13.3"
- name: Upgrade pip and setuptools
run: pip install --upgrade pip setuptools
- name: Install development dependencies
@@ -186,7 +186,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v3
with:
python-version: "3.12.6"
python-version: "3.13.3"
- name: Install development dependencies
run: pip install --user .[dev]

View File

@@ -8,22 +8,37 @@ and this project adheres to
## [Unreleased]
## Added
## [3.3.0] - 2025-05-06
- ✨(back) allow theme customnization using a configuration file #948
- ✨ Add a custom callout block to the editor #892
### Added
- ✨(backend) add endpoint checking media status #984
- ✨(backend) allow setting session cookie age via env var #977
- ✨(backend) allow theme customnization using a configuration file #948
- ✨(frontend) Add a custom callout block to the editor #892
- 🚩(frontend) version MIT only #911
- ✨(backend) integrate maleware_detection from django-lasuite #936
- 🏗️(frontend) Footer configurable #959
- 🩺(CI) add lint spell mistakes #954
- ✨(frontend) create generic theme #792
- 🛂(frontend) block edition to not connected users #945
- 🚸(frontend) Let loader during upload analyze #984
- 🚩(frontend) feature flag on blocking edition #997
## Changed
### Changed
- 📝(frontend) Update documentation
- ✅(frontend) Improve tests coverage
- 📝(frontend) Update documentation #949
- ✅(frontend) Improve tests coverage #949
- ⬆️(docker) upgrade backend image to python 3.13 #973
- ⬆️(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
- 🔥(back) remove footer endpoint #948
## [3.2.1] - 2025-05-06
@@ -575,7 +590,8 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/numerique-gouv/impress/compare/v3.2.1...main
[unreleased]: https://github.com/numerique-gouv/impress/compare/v3.3.0...main
[v3.3.0]: https://github.com/numerique-gouv/impress/releases/v3.3.0
[v3.2.1]: https://github.com/numerique-gouv/impress/releases/v3.2.1
[v3.2.0]: https://github.com/numerique-gouv/impress/releases/v3.2.0
[v3.1.0]: https://github.com/numerique-gouv/impress/releases/v3.1.0

View File

@@ -42,34 +42,38 @@ Examples of unacceptable behavior include:
## Enforcement Guidelines
- Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this
- Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of the following Code of Conduct
## Code of Conduct:
1. Correction
### 1. Correction
Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
2. Warning
### 2. Warning
Community Impact: A violation through a single incident or series of actions.
Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
3. Temporary Ban
### 3. Temporary Ban
Community Impact: A serious violation of community standards, including sustained inappropriate behavior.
Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
4. Permanent Ban
### 4. Permanent Ban
Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
Consequence: A permanent ban from any sort of public interaction within the community.
Attribution
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
Community Impact Guidelines were inspired by Mozilla's code of conduct enforcement ladder.
Community Impact Guidelines were inspired by Mozilla's [code of conduct enforcement ladder](https://github.com/mozilla/inclusion/blob/master/code-of-conduct-enforcement/consequence-ladder.md).
For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.

View File

@@ -1,7 +1,7 @@
# Django impress
# ---- base image to inherit from ----
FROM python:3.12.6-alpine3.20 AS base
FROM python:3.13.3-alpine AS base
# Upgrade pip to its latest release to speed up dependencies installation
RUN python -m pip install --upgrade pip setuptools
@@ -30,7 +30,7 @@ RUN mkdir /install && \
# ---- mails ----
FROM node:20 AS mail-builder
FROM node:24 AS mail-builder
COPY ./src/mail /mail/app
@@ -139,6 +139,9 @@ CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
# ---- Production image ----
FROM core AS backend-production
# Remove apk cache, we don't need it anymore
RUN rm -rf /var/cache/apk/*
ARG IMPRESS_STATIC_ROOT=/data/static
# Gunicorn

View File

@@ -1,13 +1,19 @@
<p align="center">
<a href="https://github.com/suitenumerique/docs">
<img alt="Docs" src="/docs/assets/docs-logo.png" width="300" />
<img alt="Docs" src="/docs/assets/banner-docs.png" width="100%" />
</a>
</p>
<p align="center">
Welcome to Docs! The open source document editor where your notes can become knowledge through live collaboration
<a href="https://github.com/suitenumerique/docs/stargazers/">
<img src="https://img.shields.io/github/stars/suitenumerique/docs" alt="">
</a>
<a href='https://github.com/suitenumerique/docs/blob/main/CONTRIBUTING.md'><img alt='PRs Welcome' src='https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=shields'/></a>
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/suitenumerique/docs"/>
<img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/suitenumerique/docs"/>
<a href="https://github.com/suitenumerique/docs/blob/main/LICENSE">
<img alt="GitHub closed issues" src="https://img.shields.io/github/license/suitenumerique/docs"/>
</a>
</p>
<p align="center">
<a href="https://matrix.to/#/#docs-official:matrix.org">
Chat on Matrix
@@ -20,43 +26,52 @@ Welcome to Docs! The open source document editor where your notes can become kno
</a>
</p>
# La Suite Docs : Collaborative Text Editing
Docs, where your notes can become knowledge through live collaboration.
<img src="/docs/assets/docs_live_collaboration_light.gif" width="100%" align="center"/>
## Why use Docs ❓
Docs is a collaborative text editor designed to address common challenges in knowledge building and sharing.
### Write
* 😌 Simple collaborative editing without the formatting complexity of markdown
* 🔌 Offline? No problem, keep writing, your edits will get synced when back online
* 💅 Create clean documents with limited but beautiful formatting options and focus on content
* 🧱 Built for productivity (markdown support, many block types, slash commands, keyboard shortcuts).
* ✨ Save time thanks to our AI actions (generate, sum up, correct, translate)
It offers a scalable and secure alternative to tools such as Google Docs, Notion (without the dbs), Outline, or Confluence.
### Collaborate
* 🤝 Collaborate with your team in real time
* 🔒 Granular access control to ensure your information is secure and only shared with the right people
* 📑 Professional document exports in multiple formats (.odt, .doc, .pdf) with customizable templates
* 📚 Built-in wiki functionality to turn your team's collaborative work into organized knowledge `ETA 05/2025`
### Write
* 😌 Get simple, accessible online editing for your team.
* 💅 Create clean documents with beautiful formatting options.
* 🖌️ Focus on your content using either the in-line editor, or [the Markdown syntax](https://www.markdownguide.org/basic-syntax/).
* 🧱 Quickly design your page thanks to the many block types, accessible from the `/` slash commands, as well as keyboard shortcuts.
* 🔌 Write offline! Your edits will be synced once you're back online.
* ✨ Save time thanks to our AI actions, such as rephrasing, summarizing, fixing typos, translating, etc. You can even turn your selected text into a prompt!
### Work together
* 🤝 Enjoy live editing! See your team collaborate in real time.
* 🔒 Keep your information secure thanks to granular access control. Only share with the right people.
* 📑 Export your content in multiple formats (`.odt`, `.docx`, `.pdf`) with customizable templates.
* 📚 Turn your team's collaborative work into organized knowledge with Subpages.
### Self-host
* 🚀 Easy to install, scalable and secure alternative to Notion, Outline or Confluence
🚀 Docs is easy to install on your own servers
⚠️ For the PDF and Docx export Docs relies on XL packages from BlockNote licenced in AGPL-3.0. Please make sure you fulfill your obligations regarding BlockNote licensing (see https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE and https://www.blocknotejs.org/about#partner-with-us).
Available methods: Helm chart, Nix package
In the works: Docker Compose, YunoHost
⚠️ For some advanced features (ex: Export as PDF) Docs relies on XL packages from BlockNote. These are licenced under AGPL-3.0 and are not MIT compatible. You can perfectly use Docs without these packages by setting the environment variable `PUBLISH_AS_MIT` to true. That way you'll build an image of the application without the features that are not MIT compatible. Read the [environment variables documentation](/docs/env.md) for more information.
## Getting started 🔧
### Test it
Test Docs on your browser by visiting this [demo document](https://impress-preprod.beta.numerique.gouv.fr/docs/6ee5aac4-4fb9-457d-95bf-bb56c2467713/)
You can test Docs on your browser by visiting this [demo document](https://impress-preprod.beta.numerique.gouv.fr/docs/6ee5aac4-4fb9-457d-95bf-bb56c2467713/)
### Run it locally
### Run Docs locally
> ⚠️ Running Docs locally using the methods described below is for testing purposes only. It is based on building Docs using Minio as the S3 storage solution but you can choose any S3 compatible object storage of your choice.
> ⚠️ The methods described below for running Docs locally is **for testing purposes only**. It is based on building Docs using [Minio](https://min.io/) as an S3-compatible storage solution. Of course you can choose any S3-compatible storage solution.
**Prerequisite**
Make sure you have a recent version of Docker and [Docker Compose](https://docs.docker.com/compose/install) installed on your laptop:
Make sure you have a recent version of Docker and [Docker Compose](https://docs.docker.com/compose/install) installed on your laptop, then type:
```shellscript
$ docker -v
@@ -68,7 +83,7 @@ $ docker compose version
Docker Compose version v2.32.4
```
> ⚠️ You may need to run the following commands with sudo but this can be avoided by adding your user to the `docker` group.
> ⚠️ You may need to run the following commands with `sudo`, but this can be avoided by adding your user to the local `docker` group.
**Project bootstrap**
@@ -78,13 +93,13 @@ The easiest way to start working on the project is to use [GNU Make](https://www
$ make bootstrap FLUSH_ARGS='--no-input'
```
This command builds the `app` container, installs dependencies, performs database migrations and compile translations. It's a good idea to use this command each time you are pulling code from the project repository to avoid dependency-related or migration-related issues.
This command builds the `app` container, installs dependencies, performs database migrations and compiles translations. It's a good idea to use this command each time you are pulling code from the project repository to avoid dependency-related or migration-related issues.
Your Docker services should now be up and running 🎉
You can access to the project by going to <http://localhost:3000>.
You will be prompted to log in, the default credentials are:
You will be prompted to log in. The default credentials are:
```
username: impress
@@ -119,13 +134,13 @@ $ make run-backend
**Adding content**
You can create a basic demo site by running:
You can create a basic demo site by running this command:
```shellscript
$ make demo
```
Finally, you can check all available Make rules using:
Finally, you can check all available Make rules using this command:
```shellscript
$ make help
@@ -133,7 +148,7 @@ $ make help
**Django admin**
You can access the Django admin site at
You can access the Django admin site at:
<http://localhost:8071/admin>.
@@ -145,7 +160,7 @@ $ make superuser
## Feedback 🙋‍♂️🙋‍♀️
We'd love to hear your thoughts and hear about your experiments, so come and say hi on [Matrix](https://matrix.to/#/#docs-official:matrix.org).
We'd love to hear your thoughts, and hear about your experiments, so come and say hi on [Matrix](https://matrix.to/#/#docs-official:matrix.org).
## Roadmap
@@ -155,7 +170,7 @@ Want to know where the project is headed? [🗺️ Checkout our roadmap](https:/
This work is released under the MIT License (see [LICENSE](https://github.com/suitenumerique/docs/blob/main/LICENSE)).
While Docs is a public driven initiative our licence choice is an invitation for private sector actors to use, sell and contribute to the project.
While Docs is a public-driven initiative, our licence choice is an invitation for private sector actors to use, sell and contribute to the project.
## Contributing 🙌
@@ -163,9 +178,9 @@ This project is intended to be community-driven, so please, do not hesitate to [
You can help us with translations on [Crowdin](https://crowdin.com/project/lasuite-docs).
If you intend to make pull requests see [CONTRIBUTING](https://github.com/suitenumerique/docs/blob/main/CONTRIBUTING.md) for guidelines.
If you intend to make pull requests, see [CONTRIBUTING](https://github.com/suitenumerique/docs/blob/main/CONTRIBUTING.md) for guidelines.
Directory structure:
## Directory structure:
```markdown
docs
@@ -183,14 +198,14 @@ docs
### Stack
Docs is built on top of [Django Rest Framework](https://www.django-rest-framework.org/), [Next.js](https://nextjs.org/), [BlockNote.js](https://www.blocknotejs.org/), [HocusPocus](https://tiptap.dev/docs/hocuspocus/introduction) and [Yjs](https://yjs.dev/).
Docs is built on top of [Django Rest Framework](https://www.django-rest-framework.org/), [Next.js](https://nextjs.org/), [BlockNote.js](https://www.blocknotejs.org/), [HocusPocus](https://tiptap.dev/docs/hocuspocus/introduction) and [Yjs](https://yjs.dev/). We thank the contributors of all these projects for their awesome work!
We are proud sponsors of [BlockNotejs](https://www.blocknotejs.org/) and [Yjs](https://yjs.dev/).
### Gov ❤️ open source
Docs is the result of a joint effort led by the French 🇫🇷🥖 ([DINUM](https://www.numerique.gouv.fr/dinum/)) and German 🇩🇪🥨 governments ([ZenDiS](https://zendis.de/)).
We are proud sponsors of [BlockNotejs](https://www.blocknotejs.org/) and [Yjs](https://yjs.dev/).
We are always looking for new public partners (we are currently onboarding the Netherlands 🇳🇱🧀), feel free to [reach out](mailto:docs@numerique.gouv.fr) if you are interested in using or contributing to Docs.
<p align="center">

View File

@@ -16,6 +16,15 @@ the following command inside your docker container:
## [Unreleased]
## [3.3.0] - 2025-05-22
⚠️ For some advanced features (ex: Export as PDF) Docs relies on XL packages from BlockNote. These are licenced under AGPL-3.0 and are not MIT compatible. You can perfectly use Docs without these packages by setting the environment variable `PUBLISH_AS_MIT` to true. That way you'll build an image of the application without the features that are not MIT compatible. Read the [environment variables documentation](/docs/docs/env.md) for more information.
The footer is now configurable from a customization file. To override the default one, you can
use the `THEME_CUSTOMIZATION_FILE_PATH` environment variable to point to your customization file.
The customization file must be a JSON file and must follow the rules described in the
[theming documentation](docs/theming.md).
## [3.0.0] - 2025-03-28
We are not using the nginx auth request anymore to access the collaboration server (`yProvider`)

View File

@@ -39,10 +39,19 @@ docker_build(
]
)
docker_build(
'localhost:5001/impress-mcp-server:latest',
context='../src/mcp_server',
dockerfile='../src/mcp_server/Dockerfile',
)
k8s_resource('impress-docs-backend-migrate', resource_deps=['postgres-postgresql'])
k8s_resource('impress-docs-backend-createsuperuser', resource_deps=['impress-docs-backend-migrate'])
k8s_resource('impress-docs-backend', resource_deps=['impress-docs-backend-migrate'])
k8s_yaml(local('cd ../src/helm && helmfile -n impress -e dev template .'))
# helmfile in docker mount the current working directory and the helmfile.yaml
# requires the keycloak config in another directory
k8s_yaml(local('cd .. && helmfile -n impress -e ${DEV_ENV:-dev} template --file ./src/helm/helmfile.yaml'))
migration = '''
set -eu

BIN
docs/assets/banner-docs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -39,7 +39,7 @@ These are the environment variables you can set for the `impress-backend` contai
| DJANGO_EMAIL_PORT | port used to connect to email host | |
| DJANGO_EMAIL_USE_TLS | use tls for email host connection | false |
| DJANGO_EMAIL_USE_SSL | use sstl for email host connection | false |
| DJANGO_EMAIL_FROM | email address used as sender | from@example.com |
| DJANGO_EMAIL_FROM | email address used as sender | from@example.com |
| DJANGO_CORS_ALLOW_ALL_ORIGINS | allow all CORS origins | true |
| DJANGO_CORS_ALLOWED_ORIGINS | list of origins allowed for CORS | [] |
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | list of origins allowed for CORS using regulair expressions | [] |
@@ -47,6 +47,7 @@ These are the environment variables you can set for the `impress-backend` contai
| COLLABORATION_API_URL | collaboration api host | |
| COLLABORATION_SERVER_SECRET | collaboration api secret | |
| COLLABORATION_WS_URL | collaboration websocket url | |
| COLLABORATION_WS_NOT_CONNECTED_READY_ONLY | Users not connected to the collaboration server cannot edit | false |
| FRONTEND_CSS_URL | To add a external css file to the app | |
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | frontend feature flag to display the homepage | false |
| FRONTEND_THEME | frontend theme to use | |
@@ -54,12 +55,13 @@ These are the environment variables you can set for the `impress-backend` contai
| CRISP_WEBSITE_ID | crisp website id for support | |
| DJANGO_CELERY_BROKER_URL | celery broker url | redis://redis:6379/0 |
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | celery broker transport options | {} |
| SESSION_COOKIE_AGE | duration of the cookie session | 60*60*12 |
| OIDC_CREATE_USER | create used on OIDC | false |
| OIDC_RP_SIGN_ALGO | verification algorithm used OIDC tokens | RS256 |
| OIDC_RP_CLIENT_ID | client id used for OIDC | impress |
| OIDC_RP_CLIENT_SECRET | client secret used for OIDC | |
| OIDC_OP_JWKS_ENDPOINT | JWKS endpoint for OIDC | |
| OIDC_OP_AUTHORIZATION_ENDPOINT | Authorization endpoint for OIDC | |
| OIDC_OP_AUTHORIZATION_ENDPOINT | Authorization endpoint for OIDC | |
| OIDC_OP_TOKEN_ENDPOINT | Token endpoint for OIDC | |
| OIDC_OP_USER_ENDPOINT | User endpoint for OIDC | |
| OIDC_OP_LOGOUT_ENDPOINT | Logout endpoint for OIDC | |
@@ -73,7 +75,7 @@ These are the environment variables you can set for the `impress-backend` contai
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed hosts for OIDC redirect url | [] |
| OIDC_STORE_ID_TOKEN | Store OIDC token | true |
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | faillback to email for identification | true |
| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false |
| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false |
| USER_OIDC_ESSENTIAL_CLAIMS | essential claims in OIDC token | [] |
| OIDC_USERINFO_FULLNAME_FIELDS | OIDC token claims to create full name | ["first_name", "last_name"] |
| OIDC_USERINFO_SHORTNAME_FIELD | OIDC token claims to create shortname | first_name |
@@ -106,8 +108,36 @@ These are the environment variables you can set for the `impress-backend` contai
These are the environment variables you can set to build the `impress-frontend` image.
Depending on how you are building the front-end application, this variable is used in different ways.
If you want to build the Docker image, this variable is used as an argument in the build command.
Example:
```
docker build -f src/frontend/Dockerfile --target frontend-production --build-arg PUBLISH_AS_MIT=false docs-frontend:latest
```
If you want to build the front-end application using the yarn build command, you can edit the file `src/frontend/apps/impress/.env` with the `NODE_ENV=production` environment variable and modify it. Alternatively, you can use the listed environment variables with the prefix `NEXT_PUBLIC_` (for example, `NEXT_PUBLIC_PUBLISH_AS_MIT=false`).
Example:
```
cd src/frontend/apps/impress
NODE_ENV=production NEXT_PUBLIC_PUBLISH_AS_MIT=false yarn build
```
| Option | Description | default |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| API_ORIGIN | backend domain - it uses the current domain if not initialized | |
| SW_DEACTIVATED | To not install the service worker | |
| PUBLISH_AS_MIT | MIT licence does not include the export feature | true |
| PUBLISH_AS_MIT | Removes packages whose licences are incompatible with the MIT licence (see below) | true |
Packages with licences incompatible with the MIT licence:
* `xl-docx-exporter`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-docx-exporter/LICENSE),
* `xl-pdf-exporter`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE)
In `.env.development`, `PUBLISH_AS_MIT` is set to `false`, allowing developers to test Docs with all its features.
⚠️ If you run Docs in production with `PUBLISH_AS_MIT` set to `false` make sure you fulfill your [BlockNote licensing](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE) or [subscription](https://www.blocknotejs.org/about#partner-with-us) obligations.

View File

@@ -30,4 +30,27 @@ body {
Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom CSS file. Once you've done this, our application will load your custom CSS file and apply the styles, changing the background color to the custom color you specified.
----
# **Footer Configuration** 📝
The footer is configurable from the theme customization file.
### Settings 🔧
```shellscript
THEME_CUSTOMIZATION_FILE_PATH=<path>
```
### Example of JSON
The json must follow some rules: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
`footer.default` is the fallback if the language is not supported.
---
Below is a visual example of a configured footer ⬇️:
![Footer Configuration Example](./assets/footer-configurable.png)

View File

@@ -61,6 +61,3 @@ COLLABORATION_BACKEND_BASE_URL=http://app-dev:8000
COLLABORATION_SERVER_ORIGIN=http://localhost:3000
COLLABORATION_SERVER_SECRET=my-secret
COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
# Frontend
FRONTEND_THEME=default

View File

@@ -1,7 +1,6 @@
# For the CI job test-e2e
SUSTAINED_THROTTLE_RATES="200/hour"
BURST_THROTTLE_RATES="200/minute"
DJANGO_SERVER_TO_SERVER_API_TOKENS=test-e2e
SUSTAINED_THROTTLE_RATES="200/hour"
Y_PROVIDER_API_KEY=yprovider-api-key
Y_PROVIDER_API_BASE_URL=http://y-provider:4444/api/
THEME_CUSTOMIZATION_FILE_PATH="" #force theme_customization to be empty

View File

@@ -26,9 +26,10 @@
"groupName": "ignored js dependencies",
"matchManagers": ["npm"],
"matchPackageNames": [
"@hocuspocus/provider",
"@hocuspocus/server",
"eslint",
"fetch-mock",
"prosemirror-model",
"node",
"node-fetch",
"workbox-webpack-plugin"

0
secu-audit.md Normal file
View File

View File

@@ -4,7 +4,7 @@
import json
import logging
import uuid
from urllib.parse import unquote, urlparse
from urllib.parse import unquote, urlencode, urlparse
from django.conf import settings
from django.contrib.postgres.aggregates import ArrayAgg
@@ -18,19 +18,22 @@ from django.db import models as db
from django.db.models.expressions import RawSQL
from django.db.models.functions import Left, Length
from django.http import Http404, StreamingHttpResponse
from django.urls import reverse
from django.utils.text import capfirst, slugify
from django.utils.translation import gettext_lazy as _
import requests
import rest_framework as drf
from botocore.exceptions import ClientError
from knox.auth import TokenAuthentication
from lasuite.malware_detection import malware_detection
from lasuite.oidc_resource_server.authentication import ResourceServerAuthentication
from rest_framework import filters, status, viewsets
from rest_framework import response as drf_response
from rest_framework.permissions import AllowAny
from rest_framework.throttling import UserRateThrottle
from core import authentication, enums, models
from core import authentication, enums, models, utils as core_utils
from core.services.ai_services import AIService
from core.services.collaboration_services import CollaborationService
from core.utils import extract_attachments, filter_descendants
@@ -429,9 +432,7 @@ class DocumentViewSet(
ordering = ["-updated_at"]
ordering_fields = ["created_at", "updated_at", "title"]
pagination_class = Pagination
permission_classes = [
permissions.DocumentAccessPermission,
]
permission_classes = [permissions.DocumentAccessPermission]
queryset = models.Document.objects.all()
serializer_class = serializers.DocumentSerializer
ai_translate_serializer_class = serializers.AITranslateSerializer
@@ -668,10 +669,14 @@ class DocumentViewSet(
return self.get_response_for_queryset(queryset)
@drf.decorators.action(
authentication_classes=[authentication.ServerToServerAuthentication],
authentication_classes=[
authentication.ServerToServerAuthentication,
ResourceServerAuthentication,
TokenAuthentication,
],
detail=False,
methods=["post"],
permission_classes=[],
permission_classes=[permissions.IsAuthenticated],
url_path="create-for-owner",
)
@transaction.atomic
@@ -1193,8 +1198,16 @@ class DocumentViewSet(
malware_detection.analyse_file(key, document_id=document.id)
url = reverse(
"documents-media-check",
kwargs={"pk": document.id},
)
parameters = urlencode({"key": key})
return drf.response.Response(
{"file": f"{settings.MEDIA_URL:s}{key:s}"},
{
"file": f"{url:s}?{parameters:s}",
},
status=drf.status.HTTP_201_CREATED,
)
@@ -1279,7 +1292,10 @@ class DocumentViewSet(
# Check if the attachment is ready
s3_client = default_storage.connection.meta.client
bucket_name = default_storage.bucket_name
head_resp = s3_client.head_object(Bucket=bucket_name, Key=key)
try:
head_resp = s3_client.head_object(Bucket=bucket_name, Key=key)
except ClientError as err:
raise drf.exceptions.PermissionDenied() from err
metadata = head_resp.get("Metadata", {})
# In order to be compatible with existing upload without `status` metadata,
# we consider them as ready.
@@ -1294,6 +1310,69 @@ class DocumentViewSet(
return drf.response.Response("authorized", headers=request.headers, status=200)
@drf.decorators.action(detail=True, methods=["get"], url_path="media-check")
def media_check(self, request, *args, **kwargs):
"""
Check if the media is ready to be served.
"""
document = self.get_object()
key = request.query_params.get("key")
if not key:
return drf.response.Response(
{"detail": "Missing 'key' query parameter"},
status=drf.status.HTTP_400_BAD_REQUEST,
)
if key not in document.attachments:
return drf.response.Response(
{"detail": "Attachment missing"},
status=drf.status.HTTP_404_NOT_FOUND,
)
# Check if the attachment is ready
s3_client = default_storage.connection.meta.client
bucket_name = default_storage.bucket_name
try:
head_resp = s3_client.head_object(Bucket=bucket_name, Key=key)
except ClientError as err:
logger.error("Client Error fetching file %s metadata: %s", key, err)
return drf.response.Response(
{"detail": "Media not found"},
status=drf.status.HTTP_404_NOT_FOUND,
)
metadata = head_resp.get("Metadata", {})
body = {
"status": metadata.get("status", enums.DocumentAttachmentStatus.PROCESSING),
}
if metadata.get("status") == enums.DocumentAttachmentStatus.READY:
body = {
"status": enums.DocumentAttachmentStatus.READY,
"file": f"{settings.MEDIA_URL:s}{key:s}",
}
return drf.response.Response(body, status=drf.status.HTTP_200_OK)
@drf.decorators.action(detail=True, methods=["get"], url_path="content")
def content(self, request, *args, **kwargs):
"""
Get the content of a document
"""
document = self.get_object()
# content_type = response.headers.get("Content-Type", "")
base64_yjs_content = document.content
content = core_utils.base64_yjs_to_markdown(base64_yjs_content)
body = {
"content": content,
}
return drf.response.Response(body, status=drf.status.HTTP_200_OK)
@drf.decorators.action(
detail=True,
methods=["post"],
@@ -1729,6 +1808,7 @@ class ConfigView(drf.views.APIView):
array_settings = [
"AI_FEATURE_ENABLED",
"COLLABORATION_WS_URL",
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY",
"CRISP_WEBSITE_ID",
"ENVIRONMENT",
"FRONTEND_CSS_URL",

View File

@@ -6,6 +6,15 @@ from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
class AuthenticatedServer:
"""
Simple class to represent an authenticated server to be used along the
IsAuthenticated permission.
"""
is_authenticated = True
class ServerToServerAuthentication(BaseAuthentication):
"""
Custom authentication class for server-to-server requests.
@@ -39,13 +48,16 @@ class ServerToServerAuthentication(BaseAuthentication):
# Validate token format and existence
auth_parts = auth_header.split(" ")
if len(auth_parts) != 2 or auth_parts[0] != self.TOKEN_TYPE:
raise AuthenticationFailed("Invalid authorization header.")
# Do not raise here to leave the door open for other authentication methods
return None
token = auth_parts[1]
if token not in settings.SERVER_TO_SERVER_API_TOKENS:
raise AuthenticationFailed("Invalid server-to-server token.")
# Do not raise here to leave the door open for other authentication methods
return None
# Authentication is successful, but no user is authenticated
# Authentication is successful
return AuthenticatedServer(), token
def authenticate_header(self, request):
"""Return the WWW-Authenticate header value."""

View File

@@ -835,9 +835,11 @@ class Document(MP_Node, BaseModel):
"ai_transform": ai_access,
"ai_translate": ai_access,
"attachment_upload": can_update,
"media_check": can_get,
"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

@@ -5,6 +5,7 @@ Test file uploads API endpoint for users in impress's core app.
import re
import uuid
from unittest import mock
from urllib.parse import parse_qs, urlparse
from django.core.files.storage import default_storage
from django.core.files.uploadedfile import SimpleUploadedFile
@@ -66,8 +67,12 @@ def test_api_documents_attachment_upload_anonymous_success():
assert response.status_code == 201
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.png")
file_path = response.json()["file"]
pattern = re.compile(rf"^{document.id!s}/attachments/(.*)\.png")
url_parsed = urlparse(response.json()["file"])
assert url_parsed.path == f"/api/v1.0/documents/{document.id!s}/media-check/"
query = parse_qs(url_parsed.query)
assert query["key"][0] is not None
file_path = query["key"][0]
match = pattern.search(file_path)
file_id = match.group(1)
# Validate that file_id is a valid UUID
@@ -148,8 +153,13 @@ def test_api_documents_attachment_upload_authenticated_success(reach, role):
assert response.status_code == 201
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.png")
match = pattern.search(response.json()["file"])
pattern = re.compile(rf"^{document.id!s}/attachments/(.*)\.png")
url_parsed = urlparse(response.json()["file"])
assert url_parsed.path == f"/api/v1.0/documents/{document.id!s}/media-check/"
query = parse_qs(url_parsed.query)
assert query["key"][0] is not None
file_path = query["key"][0]
match = pattern.search(file_path)
file_id = match.group(1)
mock_analyse_file.assert_called_once_with(
@@ -224,8 +234,12 @@ def test_api_documents_attachment_upload_success(via, role, mock_user_teams):
assert response.status_code == 201
file_path = response.json()["file"]
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.png")
pattern = re.compile(rf"^{document.id!s}/attachments/(.*)\.png")
url_parsed = urlparse(response.json()["file"])
assert url_parsed.path == f"/api/v1.0/documents/{document.id!s}/media-check/"
query = parse_qs(url_parsed.query)
assert query["key"][0] is not None
file_path = query["key"][0]
match = pattern.search(file_path)
file_id = match.group(1)
@@ -320,8 +334,13 @@ def test_api_documents_attachment_upload_fix_extension(
assert response.status_code == 201
file_path = response.json()["file"]
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.{extension:s}")
pattern = re.compile(rf"^{document.id!s}/attachments/(.*)\.{extension:s}")
url_parsed = urlparse(response.json()["file"])
assert url_parsed.path == f"/api/v1.0/documents/{document.id!s}/media-check/"
query = parse_qs(url_parsed.query)
assert query["key"][0] is not None
file_path = query["key"][0]
match = pattern.search(file_path)
file_id = match.group(1)
@@ -386,8 +405,12 @@ def test_api_documents_attachment_upload_unsafe():
assert response.status_code == 201
file_path = response.json()["file"]
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.exe")
pattern = re.compile(rf"^{document.id!s}/attachments/(.*)\.exe")
url_parsed = urlparse(response.json()["file"])
assert url_parsed.path == f"/api/v1.0/documents/{document.id!s}/media-check/"
query = parse_qs(url_parsed.query)
assert query["key"][0] is not None
file_path = query["key"][0]
match = pattern.search(file_path)
file_id = match.group(1)
@@ -410,5 +433,9 @@ def test_api_documents_attachment_upload_unsafe():
"is_unsafe": "true",
"status": "processing",
}
assert file_head["ContentType"] == "application/octet-stream"
# Depending the libmagic version, the content type may change.
assert file_head["ContentType"] in [
"application/x-dosexec",
"application/octet-stream",
]
assert file_head["ContentDisposition"] == 'attachment; filename="script.exe"'

View File

@@ -0,0 +1,244 @@
"""Test the "media_check" endpoint."""
from io import BytesIO
from uuid import uuid4
from django.core.files.storage import default_storage
import pytest
from rest_framework.test import APIClient
from core import factories
from core.enums import DocumentAttachmentStatus
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
def test_api_documents_media_check_unknown_document():
"""
The "media_check" endpoint should return a 404 error if the document does not exist.
"""
client = APIClient()
response = client.get(f"/api/v1.0/documents/{uuid4()!s}media-check/")
assert response.status_code == 404
def test_api_documents_media_check_missing_key():
"""
The "media_check" endpoint should return a 404 error if the key is missing.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user=user)
document = factories.DocumentFactory(users=[user])
response = client.get(f"/api/v1.0/documents/{document.id!s}/media-check/")
assert response.status_code == 400
assert response.json() == {"detail": "Missing 'key' query parameter"}
def test_api_documents_media_check_key_parameter_not_related_to_document():
"""
The "media_check" endpoint should return a 404 error if the key is not related to the document.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user=user)
document = factories.DocumentFactory(users=[user])
response = client.get(
f"/api/v1.0/documents/{document.id!s}/media-check/",
{"key": f"{document.id!s}/attachments/unknown.jpg"},
)
assert response.status_code == 404
assert response.json() == {"detail": "Attachment missing"}
def test_api_documents_media_check_anonymous_public_document():
"""
The "media_check" endpoint should return a 200 status code if the document is public.
"""
document = factories.DocumentFactory(link_reach="public")
filename = f"{uuid4()!s}.jpg"
key = f"{document.id!s}/attachments/{filename:s}"
default_storage.connection.meta.client.put_object(
Bucket=default_storage.bucket_name,
Key=key,
Body=BytesIO(b"my prose"),
ContentType="text/plain",
Metadata={"status": DocumentAttachmentStatus.PROCESSING},
)
document.attachments = [key]
document.save(update_fields=["attachments"])
client = APIClient()
response = client.get(
f"/api/v1.0/documents/{document.id!s}/media-check/", {"key": key}
)
assert response.status_code == 200
assert response.json() == {"status": DocumentAttachmentStatus.PROCESSING}
def test_api_documents_media_check_anonymous_public_document_ready():
"""
The "media_check" endpoint should return a 200 status code if the document is public.
"""
document = factories.DocumentFactory(link_reach="public")
filename = f"{uuid4()!s}.jpg"
key = f"{document.id!s}/attachments/{filename:s}"
default_storage.connection.meta.client.put_object(
Bucket=default_storage.bucket_name,
Key=key,
Body=BytesIO(b"my prose"),
ContentType="text/plain",
Metadata={"status": DocumentAttachmentStatus.READY},
)
document.attachments = [key]
document.save(update_fields=["attachments"])
client = APIClient()
response = client.get(
f"/api/v1.0/documents/{document.id!s}/media-check/", {"key": key}
)
assert response.status_code == 200
assert response.json() == {
"status": DocumentAttachmentStatus.READY,
"file": f"/media/{key:s}",
}
@pytest.mark.parametrize("link_reach", ["restricted", "authenticated"])
def test_api_documents_media_check_anonymous_non_public_document(link_reach):
"""
The "media_check" endpoint should return a 403 error if the document is not public.
"""
document = factories.DocumentFactory(link_reach=link_reach)
client = APIClient()
response = client.get(f"/api/v1.0/documents/{document.id!s}/media-check/")
assert response.status_code == 401
def test_api_documents_media_check_connected_document():
"""
The "media_check" endpoint should return a 200 status code for a user connected
checking for a document with link_reach authenticated.
"""
document = factories.DocumentFactory(link_reach="authenticated")
filename = f"{uuid4()!s}.jpg"
key = f"{document.id!s}/attachments/{filename:s}"
default_storage.connection.meta.client.put_object(
Bucket=default_storage.bucket_name,
Key=key,
Body=BytesIO(b"my prose"),
ContentType="text/plain",
Metadata={"status": DocumentAttachmentStatus.READY},
)
document.attachments = [key]
document.save(update_fields=["attachments"])
user = factories.UserFactory()
client = APIClient()
client.force_login(user=user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/media-check/", {"key": key}
)
assert response.status_code == 200
assert response.json() == {
"status": DocumentAttachmentStatus.READY,
"file": f"/media/{key:s}",
}
def test_api_documents_media_check_connected_document_media_not_related():
"""
The "media_check" endpoint should return a 404 error if the key is not related to the document.
"""
document = factories.DocumentFactory(link_reach="authenticated")
filename = f"{uuid4()!s}.jpg"
key = f"{document.id!s}/attachments/{filename:s}"
user = factories.UserFactory()
client = APIClient()
client.force_login(user=user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/media-check/", {"key": key}
)
assert response.status_code == 404
assert response.json() == {"detail": "Attachment missing"}
def test_api_documents_media_check_media_missing_on_storage():
"""
The "media_check" endpoint should return a 404 error if the media is missing on storage.
"""
document = factories.DocumentFactory(link_reach="authenticated")
filename = f"{uuid4()!s}.jpg"
key = f"{document.id!s}/attachments/{filename:s}"
document.attachments = [key]
document.save(update_fields=["attachments"])
user = factories.UserFactory()
client = APIClient()
client.force_login(user=user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/media-check/", {"key": key}
)
assert response.status_code == 404
assert response.json() == {"detail": "Media not found"}
@pytest.mark.parametrize("via", VIA)
def test_api_documents_media_check_restricted_document(via, mock_user_teams):
"""
The "media_check" endpoint should return a 200 status code if the document is restricted and
the user has access to it.
"""
document = factories.DocumentFactory(link_reach="restricted")
filename = f"{uuid4()!s}.jpg"
key = f"{document.id!s}/attachments/{filename:s}"
default_storage.connection.meta.client.put_object(
Bucket=default_storage.bucket_name,
Key=key,
Body=BytesIO(b"my prose"),
ContentType="text/plain",
Metadata={"status": DocumentAttachmentStatus.READY},
)
document.attachments = [key]
document.save(update_fields=["attachments"])
user = factories.UserFactory()
client = APIClient()
client.force_login(user=user)
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(document=document, team="lasuite")
response = client.get(
f"/api/v1.0/documents/{document.id!s}/media-check/", {"key": key}
)
assert response.status_code == 200
assert response.json() == {
"status": DocumentAttachmentStatus.READY,
"file": f"/media/{key:s}",
}

View File

@@ -48,6 +48,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": document.link_role == "editor",
"restore": False,
@@ -111,6 +112,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
"link_configuration": False,
"link_select_options": models.LinkReachChoices.get_select_options(links),
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": grand_parent.link_role == "editor",
"restore": False,
@@ -210,6 +212,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": document.link_role == "editor",
"restore": False,
@@ -279,8 +282,9 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
"invite_owner": False,
"link_configuration": False,
"link_select_options": models.LinkReachChoices.get_select_options(links),
"move": False,
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": grand_parent.link_role == "editor",
"restore": False,
"retrieve": True,
@@ -460,6 +464,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
"link_configuration": access.role in ["administrator", "owner"],
"link_select_options": models.LinkReachChoices.get_select_options(links),
"media_auth": True,
"media_check": True,
"move": access.role in ["administrator", "owner"],
"partial_update": access.role != "reader",
"restore": access.role == "owner",

View File

@@ -91,6 +91,7 @@ def test_api_documents_trashbin_format():
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": False, # Can't move a deleted document
"partial_update": True,
"restore": True,

View File

@@ -20,6 +20,7 @@ pytestmark = pytest.mark.django_db
@override_settings(
AI_FEATURE_ENABLED=False,
COLLABORATION_WS_URL="http://testcollab/",
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY=True,
CRISP_WEBSITE_ID="123",
FRONTEND_CSS_URL="http://testcss/",
FRONTEND_THEME="test-theme",
@@ -41,6 +42,7 @@ def test_api_config(is_authenticated):
assert response.status_code == HTTP_200_OK
assert response.json() == {
"COLLABORATION_WS_URL": "http://testcollab/",
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY": True,
"CRISP_WEBSITE_ID": "123",
"ENVIRONMENT": "test",
"FRONTEND_CSS_URL": "http://testcss/",

View File

@@ -165,6 +165,7 @@ def test_models_documents_get_abilities_forbidden(
"favorite": False,
"invite_owner": False,
"media_auth": False,
"media_check": False,
"move": False,
"link_configuration": False,
"link_select_options": {
@@ -231,6 +232,7 @@ def test_models_documents_get_abilities_reader(
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": False,
"restore": False,
@@ -293,6 +295,7 @@ def test_models_documents_get_abilities_editor(
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": True,
"restore": False,
@@ -344,6 +347,7 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": True,
"partial_update": True,
"restore": True,
@@ -392,6 +396,7 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": True,
"partial_update": True,
"restore": False,
@@ -443,6 +448,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": True,
"restore": False,
@@ -501,6 +507,7 @@ def test_models_documents_get_abilities_reader_user(
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": access_from_link,
"restore": False,
@@ -557,6 +564,7 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": False,
"restore": False,

View File

@@ -0,0 +1,131 @@
"""
Test user_token API endpoints in the impress core app.
"""
import pytest
from knox.models import get_token_model
from rest_framework.test import APIClient
from core import factories, models
pytestmark = pytest.mark.django_db
AuthToken = get_token_model()
def test_api_user_token_list_anonymous(client):
"""Anonymous users should not be allowed to list user tokens."""
response = client.get("/api/v1.0/user-tokens/")
assert response.status_code == 403
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
def test_api_user_token_list_authenticated(client):
"""
Authenticated users should be able to list their own tokens.
Tokens are identified by digest, and include created/expiry.
"""
user = factories.UserFactory()
# Knox creates a token instance and a character string token key.
# The create method returns a tuple: (instance, token_key_string)
token_instance_1, _ = AuthToken.objects.create(user=user)
AuthToken.objects.create(user=user) # Another token for the same user
AuthToken.objects.create(user=factories.UserFactory()) # Token for a different user
client.force_login(user)
response = client.get("/api/v1.0/user-tokens/")
assert response.status_code == 200
content = response.json()
assert len(content) == 2
# Check that the response contains the digests of the tokens created for the user
response_token_digests = {item["digest"] for item in content}
assert token_instance_1.digest in response_token_digests
# Ensure the token_key is not listed
for item in content:
assert "token_key" not in item
assert "digest" in item
assert "created" in item
assert "expiry" in item
def test_api_user_token_create_anonymous(client):
"""Anonymous users should not be allowed to create user tokens."""
# The create endpoint does not take any parameters as per TokenCreateSerializer
# (user is implicit, other fields are read_only)
response = client.post("/api/v1.0/user-tokens/", data={})
assert response.status_code == 403
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
def test_api_user_token_create_authenticated(client):
"""
Authenticated users should be able to create a new token.
The token key should be returned in the response upon creation.
"""
user = factories.UserFactory()
client.force_login(user)
# The create endpoint does not take any parameters as per TokenCreateSerializer
response = client.post("/api/v1.0/user-tokens/", data={})
assert response.status_code == 201
content = response.json()
# Based on TokenCreateSerializer, these fields should be in the response
assert "token_key" in content
assert "digest" in content
assert "created" in content
assert "expiry" in content
assert len(content["token_key"]) > 0 # Knox token key should be non-empty
# Verify the token was actually created in the database for the user
assert AuthToken.objects.filter(user=user, digest=content["digest"]).exists()
def test_api_user_token_destroy_anonymous(client):
"""Anonymous users should not be allowed to delete user tokens."""
user = factories.UserFactory()
token_instance, _ = AuthToken.objects.create(user=user)
response = client.delete(f"/api/v1.0/user-tokens/{token_instance.digest}/")
assert response.status_code == 403
assert AuthToken.objects.filter(digest=token_instance.digest).exists()
def test_api_user_token_destroy_authenticated_own_token(client):
"""Authenticated users should be able to delete their own tokens."""
user = factories.UserFactory()
token_instance, _ = AuthToken.objects.create(user=user)
client.force_login(user)
response = client.delete(f"/api/v1.0/user-tokens/{token_instance.digest}/")
assert response.status_code == 204
assert not AuthToken.objects.filter(digest=token_instance.digest).exists()
def test_api_user_token_destroy_authenticated_other_user_token(client):
"""Authenticated users should not be able to delete other users' tokens."""
user = factories.UserFactory()
other_user = factories.UserFactory()
other_user_token_instance, _ = AuthToken.objects.create(user=other_user)
client.force_login(user) # Log in as 'user'
response = client.delete(f"/api/v1.0/user-tokens/{other_user_token_instance.digest}/")
# The default behavior for a non-found or non-permissioned item in DestroyModelMixin
# when the queryset is filtered (as in get_queryset) is often a 404.
assert response.status_code == 404
assert AuthToken.objects.filter(digest=other_user_token_instance.digest).exists()
def test_api_user_token_destroy_non_existent_token(client):
"""Attempting to delete a non-existent token should result in a 404."""
user = factories.UserFactory()
client.force_login(user)
response = client.delete("/api/v1.0/user-tokens/nonexistentdigest/")
assert response.status_code == 404

View File

@@ -4,15 +4,22 @@ from django.conf import settings
from django.urls import include, path, re_path
from lasuite.oidc_login.urls import urlpatterns as oidc_urls
from lasuite.oidc_resource_server.urls import urlpatterns as resource_server_urls
from rest_framework.routers import DefaultRouter
from core.api import viewsets
from core.user_token import viewsets as user_token_viewsets
# - Main endpoints
router = DefaultRouter()
router.register("templates", viewsets.TemplateViewSet, basename="templates")
router.register("documents", viewsets.DocumentViewSet, basename="documents")
router.register("users", viewsets.UserViewSet, basename="users")
router.register(
"user-tokens",
user_token_viewsets.UserTokenViewset,
basename="user_tokens",
)
# - Routes nested under a document
document_related_router = DefaultRouter()
@@ -44,6 +51,7 @@ urlpatterns = [
[
*router.urls,
*oidc_urls,
*resource_server_urls,
re_path(
r"^documents/(?P<resource_id>[0-9a-z-]*)/",
include(document_related_router.urls),

View File

View File

@@ -0,0 +1,27 @@
from knox.models import get_token_model
from rest_framework import serializers
class TokenReadSerializer(serializers.ModelSerializer):
"""Serialize token for list purpose."""
class Meta:
model = get_token_model()
fields = ["digest", "created", "expiry"]
read_only_fields = ["digest", "created", "expiry"]
class TokenCreateSerializer(serializers.ModelSerializer):
"""Serialize token for creation purpose."""
class Meta:
model = get_token_model()
fields = ["user", "digest", "token_key", "created", "expiry"]
read_only_fields = ["digest", "token_key", "created", "expiry"]
extra_kwargs = {"user": {"write_only": True}}
def create(self, validated_data):
"""The default knox token create manager returns a tuple."""
instance, token = super().create(validated_data)
instance.token_key = token # warning do not save this
return instance

View File

@@ -0,0 +1,50 @@
"""API endpoints for user token management"""
from knox.models import get_token_model
from rest_framework import permissions, viewsets, mixins
from rest_framework.authentication import SessionAuthentication
from . import serializers
class UserTokenViewset(
mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
"""API ViewSet for user invitations to document.
This view access is restricted to the session ie from frontend.
GET /api/v1.0/user-token/
Return list of existing tokens.
POST /api/v1.0/user-token/
Return newly created token.
DELETE /api/v1.0/user-token/<token_id>/
Delete targeted token.
"""
authentication_classes = [SessionAuthentication]
pagination_class = None
permission_classes = [permissions.IsAuthenticated]
queryset = get_token_model().objects.all()
serializer_class = serializers.TokenReadSerializer
def get_queryset(self):
"""Return the queryset restricted to the logged-in user."""
queryset = super().get_queryset()
queryset = queryset.filter(user_id=self.request.user.pk)
return queryset
def get_serializer_class(self):
if self.action == "create":
return serializers.TokenCreateSerializer
return super().get_serializer_class()
def create(self, request, *args, **kwargs):
"""Enforce request data to use current user."""
request.data["user"] = self.request.user.pk
return super().create(request, *args, **kwargs)

View File

@@ -66,6 +66,116 @@ def base64_yjs_to_text(base64_string):
soup = BeautifulSoup(blocknote_structure, "lxml-xml")
return soup.get_text(separator=" ", strip=True)
def base64_yjs_to_markdown(base64_string: str) -> str:
xml_content = base64_yjs_to_xml(base64_string)
soup = BeautifulSoup(xml_content, "lxml-xml")
md_lines: list[str] = []
def walk(node) -> None:
if not getattr(node, "name", None):
return
# Treat the synthetic “[document]” tag exactly like a wrapper
if node.name in {"[document]", "blockGroup", "blockContainer"}:
for child in node.find_all(recursive=False):
walk(child)
if node.name == "blockContainer":
md_lines.append("") # paragraph break
return
# ----------- content nodes -------------
if node.name == "heading":
level = int(node.get("level", 1))
md_lines.extend([("#" * level) + " " + process_inline_formatting(node), ""])
elif node.name == "paragraph":
md_lines.extend([process_inline_formatting(node), ""])
elif node.name == "bulletListItem":
md_lines.append("- " + process_inline_formatting(node))
elif node.name == "numberedListItem":
idx = node.get("index", "1")
md_lines.append(f"{idx}. " + process_inline_formatting(node))
elif node.name == "checkListItem":
checked = "x" if node.get("checked") == "true" else " "
md_lines.append(f"- [{checked}] " + process_inline_formatting(node))
elif node.name == "codeBlock":
lang = node.get("language", "")
code = node.get_text("", strip=False)
md_lines.extend([f"```{lang}", code, "```", ""])
elif node.name in {"quote", "blockquote"}:
quote = process_inline_formatting(node)
for line in quote.splitlines() or [""]:
md_lines.append("> " + line)
md_lines.append("")
elif node.name == "divider":
md_lines.extend(["---", ""])
elif node.name == "callout":
emoji = node.get("emoji", "💡")
md_lines.extend([f"> {emoji} {process_inline_formatting(node)}", ""])
elif node.name == "img":
src = node.get("src", "")
alt = node.get("alt", "")
md_lines.extend([f"![{alt}]({src})", ""])
# unknown tags are ignored
# kick-off: start at the synthetic root
walk(soup)
# collapse accidental multiple blank lines
cleaned: list[str] = []
for line in md_lines:
if line == "" and (not cleaned or cleaned[-1] == ""):
continue
cleaned.append(line)
return "\n".join(cleaned).rstrip() + "\n"
def process_inline_formatting(element):
"""
Process inline formatting elements like bold, italic, underline, etc.
and convert them to markdown syntax.
"""
result = ""
# If it's just a text node, return the text
if isinstance(element, str):
return element
# Process children elements
for child in element.contents:
if isinstance(child, str):
result += child
elif hasattr(child, 'name'):
if child.name == "bold":
result += "**" + process_inline_formatting(child) + "**"
elif child.name == "italic":
result += "*" + process_inline_formatting(child) + "*"
elif child.name == "underline":
result += "__" + process_inline_formatting(child) + "__"
elif child.name == "strike":
result += "~~" + process_inline_formatting(child) + "~~"
elif child.name == "code":
result += "`" + process_inline_formatting(child) + "`"
elif child.name == "link":
href = child.get("href", "")
text = process_inline_formatting(child)
result += f"[{text}]({href})"
else:
# For other elements, just process their contents
result += process_inline_formatting(child)
return result
def extract_attachments(content):
"""Helper method to extract media paths from a document's content."""

View File

@@ -1,124 +1,129 @@
{
"footer": {
"default": {
"externalLinks": [
{
"label": "Github",
"href": "https://github.com/suitenumerique/docs/"
},
{
"label": "DINUM",
"href": "https://www.numerique.gouv.fr/dinum/"
},
{
"label": "ZenDiS",
"href": "https://zendis.de/"
},
{
"label": "BlockNote.js",
"href": "https://www.blocknotejs.org/"
}
],
"bottomInformation": {
"label": "Unless otherwise stated, all content on this site is under",
"link": {
"label": "licence etalab-2.0",
"href": "https://github.com/etalab/licence-ouverte/blob/master/LO.md"
}
}
"footer": {
"default": {
"logo": {
"src": "/assets/icon-docs.svg",
"width": "54px",
"alt": "Docs Logo",
"withTitle": true
},
"en": {
"legalLinks": [
{
"label": "Legal Notice",
"href": "#"
},
{
"label": "Personal data and cookies",
"href": "#"
},
{
"label": "Accessibility",
"href": "#"
}
],
"bottomInformation": {
"label": "Unless otherwise stated, all content on this site is under",
"link": {
"label": "licence MIT",
"href": "https://github.com/suitenumerique/docs/blob/main/LICENSE"
}
"externalLinks": [
{
"label": "Github",
"href": "https://github.com/suitenumerique/docs/"
},
{
"label": "DINUM",
"href": "https://www.numerique.gouv.fr/dinum/"
},
{
"label": "ZenDiS",
"href": "https://zendis.de/"
},
{
"label": "BlockNote.js",
"href": "https://www.blocknotejs.org/"
}
},
"fr": {
"legalLinks": [
{
"label": "Mentions légales",
"href": "#"
},
{
"label": "Données personnelles et cookies",
"href": "#"
},
{
"label": "Accessibilité",
"href": "#"
}
],
"bottomInformation": {
"label": "Sauf mention contraire, tout le contenu de ce site est sous",
"link": {
"label": "licence MIT",
"href": "https://github.com/suitenumerique/docs/blob/main/LICENSE"
}
],
"bottomInformation": {
"label": "Unless otherwise stated, all content on this site is under",
"link": {
"label": "licence etalab-2.0",
"href": "https://github.com/etalab/licence-ouverte/blob/master/LO.md"
}
},
"de": {
"legalLinks": [
{
"label": "Impressum",
"href": "#"
},
{
"label": "Personenbezogene Daten und Cookies",
"href": "#"
},
{
"label": "Barrierefreiheit",
"href": "#"
}
],
"bottomInformation": {
"label": "Sofern nicht anders angegeben, steht der gesamte Inhalt dieser Website unter",
"link": {
"label": "licence MIT",
"href": "https://github.com/suitenumerique/docs/blob/main/LICENSE"
}
}
},
"en": {
"legalLinks": [
{
"label": "Legal Notice",
"href": "#"
},
{
"label": "Personal data and cookies",
"href": "#"
},
{
"label": "Accessibility",
"href": "#"
}
},
"nl": {
"legalLinks": [
{
"label": "Wettelijke bepalingen",
"href": "#"
},
{
"label": "Persoonlijke gegevens en cookies",
"href": "#"
},
{
"label": "Toegankelijkheid",
"href": "#"
}
],
"bottomInformation": {
"label": "Tenzij anders vermeld, is alle inhoud van deze site ondergebracht onder",
"link": {
"label": "licence MIT",
"href": "https://github.com/suitenumerique/docs/blob/main/LICENSE"
}
],
"bottomInformation": {
"label": "Unless otherwise stated, all content on this site is under",
"link": {
"label": "licence MIT",
"href": "https://github.com/suitenumerique/docs/blob/main/LICENSE"
}
}
},
"fr": {
"legalLinks": [
{
"label": "Mentions légales",
"href": "#"
},
{
"label": "Données personnelles et cookies",
"href": "#"
},
{
"label": "Accessibilité",
"href": "#"
}
],
"bottomInformation": {
"label": "Sauf mention contraire, tout le contenu de ce site est sous",
"link": {
"label": "licence MIT",
"href": "https://github.com/suitenumerique/docs/blob/main/LICENSE"
}
}
},
"de": {
"legalLinks": [
{
"label": "Impressum",
"href": "#"
},
{
"label": "Personenbezogene Daten und Cookies",
"href": "#"
},
{
"label": "Barrierefreiheit",
"href": "#"
}
],
"bottomInformation": {
"label": "Sofern nicht anders angegeben, steht der gesamte Inhalt dieser Website unter",
"link": {
"label": "licence MIT",
"href": "https://github.com/suitenumerique/docs/blob/main/LICENSE"
}
}
},
"nl": {
"legalLinks": [
{
"label": "Wettelijke bepalingen",
"href": "#"
},
{
"label": "Persoonlijke gegevens en cookies",
"href": "#"
},
{
"label": "Toegankelijkheid",
"href": "#"
}
],
"bottomInformation": {
"label": "Tenzij anders vermeld, is alle inhoud van deze site ondergebracht onder",
"link": {
"label": "licence MIT",
"href": "https://github.com/suitenumerique/docs/blob/main/LICENSE"
}
}
}
}
}

View File

@@ -10,6 +10,9 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
# pylint: disable=too-many-lines
import datetime
import os
import tomllib
from socket import gethostbyname, gethostname
@@ -303,6 +306,7 @@ class Base(Configuration):
"django_filters",
"dockerflow.django",
"rest_framework",
"knox",
"parler",
"treebeard",
"easy_thumbnails",
@@ -327,8 +331,9 @@ class Base(Configuration):
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"mozilla_django_oidc.contrib.drf.OIDCAuthentication",
"rest_framework.authentication.SessionAuthentication",
"knox.auth.TokenAuthentication",
"lasuite.oidc_resource_server.authentication.ResourceServerAuthentication",
),
"DEFAULT_PARSER_CLASSES": [
"rest_framework.parsers.JSONParser",
@@ -413,6 +418,11 @@ class Base(Configuration):
COLLABORATION_WS_URL = values.Value(
None, environ_name="COLLABORATION_WS_URL", environ_prefix=None
)
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = values.BooleanValue(
False,
environ_name="COLLABORATION_WS_NOT_CONNECTED_READY_ONLY",
environ_prefix=None,
)
# Frontend
FRONTEND_THEME = values.Value(
@@ -462,7 +472,9 @@ class Base(Configuration):
# Session
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
SESSION_COOKIE_AGE = 60 * 60 * 12
SESSION_COOKIE_AGE = values.PositiveIntegerValue(
default=60 * 60 * 12, environ_name="SESSION_COOKIE_AGE", environ_prefix=None
)
# OIDC - Authorization Code Flow
OIDC_CREATE_USER = values.BooleanValue(
@@ -586,6 +598,72 @@ class Base(Configuration):
default=True, environ_name="ALLOW_LOGOUT_GET_METHOD", environ_prefix=None
)
# OIDC - Docs as a resource server
OIDC_OP_URL = values.Value(
default=None, environ_name="OIDC_OP_URL", environ_prefix=None
)
OIDC_OP_INTROSPECTION_ENDPOINT = values.Value(
environ_name="OIDC_OP_INTROSPECTION_ENDPOINT", environ_prefix=None
)
OIDC_VERIFY_SSL = values.BooleanValue(
default=True, environ_name="OIDC_VERIFY_SSL", environ_prefix=None
)
OIDC_TIMEOUT = values.IntegerValue(
default=3, environ_name="OIDC_TIMEOUT", environ_prefix=None
)
OIDC_PROXY = values.Value(None, environ_name="OIDC_PROXY", environ_prefix=None)
OIDC_RS_BACKEND_CLASS = "lasuite.oidc_resource_server.backend.ResourceServerBackend"
OIDC_RS_AUDIENCE_CLAIM = values.Value( # The claim used to identify the audience
default="client_id", environ_name="OIDC_RS_AUDIENCE_CLAIM", environ_prefix=None
)
OIDC_RS_PRIVATE_KEY_STR = values.Value(
default=None,
environ_name="OIDC_RS_PRIVATE_KEY_STR",
environ_prefix=None,
)
OIDC_RS_ENCRYPTION_KEY_TYPE = values.Value(
default="RSA",
environ_name="OIDC_RS_ENCRYPTION_KEY_TYPE",
environ_prefix=None,
)
OIDC_RS_ENCRYPTION_ALGO = values.Value(
default="RSA-OAEP",
environ_name="OIDC_RS_ENCRYPTION_ALGO",
environ_prefix=None,
)
OIDC_RS_ENCRYPTION_ENCODING = values.Value(
default="A256GCM",
environ_name="OIDC_RS_ENCRYPTION_ENCODING",
environ_prefix=None,
)
OIDC_RS_CLIENT_ID = values.Value(
None, environ_name="OIDC_RS_CLIENT_ID", environ_prefix=None
)
OIDC_RS_CLIENT_SECRET = values.Value(
None,
environ_name="OIDC_RS_CLIENT_SECRET",
environ_prefix=None,
)
OIDC_RS_SIGNING_ALGO = values.Value(
default="ES256", environ_name="OIDC_RS_SIGNING_ALGO", environ_prefix=None
)
OIDC_RS_SCOPES = values.ListValue(
[], environ_name="OIDC_RS_SCOPES", environ_prefix=None
)
# User token (knox)
REST_KNOX = {
"SECURE_HASH_ALGORITHM": "hashlib.sha512",
"AUTH_TOKEN_CHARACTER_LENGTH": 64,
"TOKEN_TTL": datetime.timedelta(hours=24 * 7),
"TOKEN_LIMIT_PER_USER": None,
"AUTO_REFRESH": False,
"AUTO_REFRESH_MAX_TTL": None,
"MIN_REFRESH_INTERVAL": 60,
"AUTH_HEADER_PREFIX": "Token",
}
# AI service
AI_FEATURE_ENABLED = values.BooleanValue(
default=False, environ_name="AI_FEATURE_ENABLED", environ_prefix=None
@@ -677,6 +755,15 @@ class Base(Configuration):
),
"propagate": False,
},
"docs.security": {
"handlers": ["console"],
"level": values.Value(
"INFO",
environ_name="LOGGING_LEVEL_LOGGERS_SECURITY",
environ_prefix=None,
),
"propagate": False,
},
},
}

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 11:48+0000\n"
"PO-Revision-Date: 2025-05-05 07:07\n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -66,32 +66,32 @@ msgstr "Doare korf"
msgid "Format"
msgstr "Stumm"
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr "eilenn {title}"
#: build/lib/core/enums.py:35 core/enums.py:35
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr "Bugel kentañ"
#: build/lib/core/enums.py:36 core/enums.py:36
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr "Bugel diwezhañ"
#: build/lib/core/enums.py:37 core/enums.py:37
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr ""
#: build/lib/core/enums.py:38 core/enums.py:38
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr ""
#: build/lib/core/enums.py:39 core/enums.py:39
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr "Kleiz"
#: build/lib/core/enums.py:40 core/enums.py:40
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "Dehoù"
@@ -225,8 +225,8 @@ msgstr "implijer"
msgid "users"
msgstr "implijerien"
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
#: core/models.py:470 core/models.py:1154
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr "titl"
@@ -242,128 +242,128 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
#: core/models.py:872
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:907 core/models.py:907
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:911 core/models.py:911
#: build/lib/core/models.py:912 core/models.py:912
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:917 core/models.py:917
#: build/lib/core/models.py:918 core/models.py:918
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1015 core/models.py:1015
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1016 core/models.py:1016
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1046 core/models.py:1046
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1052 core/models.py:1052
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1074 core/models.py:1074
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1075 core/models.py:1075
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1081 core/models.py:1081
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1087 core/models.py:1087
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1093 build/lib/core/models.py:1241
#: core/models.py:1093 core/models.py:1241
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1155 core/models.py:1155
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr ""
#: build/lib/core/models.py:1156 core/models.py:1156
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr ""
#: build/lib/core/models.py:1157 core/models.py:1157
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1159 core/models.py:1159
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr "publik"
#: build/lib/core/models.py:1161 core/models.py:1161
#: build/lib/core/models.py:1162 core/models.py:1162
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1167 core/models.py:1167
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr "Patrom"
#: build/lib/core/models.py:1168 core/models.py:1168
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr "Patromoù"
#: build/lib/core/models.py:1222 core/models.py:1222
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1223 core/models.py:1223
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1229 core/models.py:1229
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1235 core/models.py:1235
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1258 core/models.py:1258
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1299 core/models.py:1299
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-04-29 11:48+0000\n"
"PO-Revision-Date: 2025-05-05 07:07\n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -66,32 +66,32 @@ msgstr "Typ"
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr "Kopie von {title}"
#: build/lib/core/enums.py:35 core/enums.py:35
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr "Erstes Unterelement"
#: build/lib/core/enums.py:36 core/enums.py:36
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr "Letztes Unterelement"
#: build/lib/core/enums.py:37 core/enums.py:37
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr "Erstes Nebenelement"
#: build/lib/core/enums.py:38 core/enums.py:38
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr "Letztes Nebenelement"
#: build/lib/core/enums.py:39 core/enums.py:39
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr "Links"
#: build/lib/core/enums.py:40 core/enums.py:40
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "Rechts"
@@ -225,8 +225,8 @@ msgstr "Benutzer"
msgid "users"
msgstr "Benutzer"
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
#: core/models.py:470 core/models.py:1154
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr "Titel"
@@ -242,128 +242,128 @@ msgstr "Dokument"
msgid "Documents"
msgstr "Dokumente"
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
#: core/models.py:872
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: build/lib/core/models.py:907 core/models.py:907
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
#: build/lib/core/models.py:911 core/models.py:911
#: build/lib/core/models.py:912 core/models.py:912
#, 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:917 core/models.py:917
#: build/lib/core/models.py:918 core/models.py:918
#, 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:1015 core/models.py:1015
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:1016 core/models.py:1016
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
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:1045 core/models.py:1045
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:1046 core/models.py:1046
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:1052 core/models.py:1052
#: build/lib/core/models.py:1053 core/models.py:1053
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:1074 core/models.py:1074
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:1075 core/models.py:1075
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:1081 core/models.py:1081
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1087 core/models.py:1087
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1093 build/lib/core/models.py:1241
#: core/models.py:1093 core/models.py:1241
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
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:1155 core/models.py:1155
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr "Beschreibung"
#: build/lib/core/models.py:1156 core/models.py:1156
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr "Code"
#: build/lib/core/models.py:1157 core/models.py:1157
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1159 core/models.py:1159
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr "öffentlich"
#: build/lib/core/models.py:1161 core/models.py:1161
#: build/lib/core/models.py:1162 core/models.py:1162
msgid "Whether this template is public for anyone to use."
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
#: build/lib/core/models.py:1167 core/models.py:1167
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr "Vorlage"
#: build/lib/core/models.py:1168 core/models.py:1168
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr "Vorlagen"
#: build/lib/core/models.py:1222 core/models.py:1222
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr "Vorlage/Benutzer-Beziehung"
#: build/lib/core/models.py:1223 core/models.py:1223
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr "Vorlage/Benutzerbeziehungen"
#: build/lib/core/models.py:1229 core/models.py:1229
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
#: build/lib/core/models.py:1235 core/models.py:1235
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr "Dieses Team ist bereits in diesem Template."
#: build/lib/core/models.py:1258 core/models.py:1258
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1299 core/models.py:1299
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-04-29 11:48+0000\n"
"PO-Revision-Date: 2025-05-05 07:07\n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -66,32 +66,32 @@ msgstr ""
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr ""
#: build/lib/core/enums.py:35 core/enums.py:35
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr ""
#: build/lib/core/enums.py:36 core/enums.py:36
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr ""
#: build/lib/core/enums.py:37 core/enums.py:37
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr ""
#: build/lib/core/enums.py:38 core/enums.py:38
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr ""
#: build/lib/core/enums.py:39 core/enums.py:39
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr ""
#: build/lib/core/enums.py:40 core/enums.py:40
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr ""
@@ -225,8 +225,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
#: core/models.py:470 core/models.py:1154
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr ""
@@ -242,128 +242,128 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
#: core/models.py:872
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:907 core/models.py:907
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:911 core/models.py:911
#: build/lib/core/models.py:912 core/models.py:912
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:917 core/models.py:917
#: build/lib/core/models.py:918 core/models.py:918
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1015 core/models.py:1015
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1016 core/models.py:1016
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1046 core/models.py:1046
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1052 core/models.py:1052
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1074 core/models.py:1074
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1075 core/models.py:1075
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1081 core/models.py:1081
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1087 core/models.py:1087
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1093 build/lib/core/models.py:1241
#: core/models.py:1093 core/models.py:1241
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1155 core/models.py:1155
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr ""
#: build/lib/core/models.py:1156 core/models.py:1156
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr ""
#: build/lib/core/models.py:1157 core/models.py:1157
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr ""
#: build/lib/core/models.py:1159 core/models.py:1159
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr ""
#: build/lib/core/models.py:1161 core/models.py:1161
#: build/lib/core/models.py:1162 core/models.py:1162
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1167 core/models.py:1167
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1168 core/models.py:1168
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1222 core/models.py:1222
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1223 core/models.py:1223
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1229 core/models.py:1229
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1235 core/models.py:1235
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1258 core/models.py:1258
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1299 core/models.py:1299
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-04-29 11:48+0000\n"
"PO-Revision-Date: 2025-05-05 07:07\n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -66,32 +66,32 @@ msgstr "Tipo de Cuerpo"
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr "copia de {title}"
#: build/lib/core/enums.py:35 core/enums.py:35
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr "Primer nodo"
#: build/lib/core/enums.py:36 core/enums.py:36
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr "Último nodo"
#: build/lib/core/enums.py:37 core/enums.py:37
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr "Primera relación"
#: build/lib/core/enums.py:38 core/enums.py:38
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr "Última relación"
#: build/lib/core/enums.py:39 core/enums.py:39
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr "Izquierda"
#: build/lib/core/enums.py:40 core/enums.py:40
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "Derecha"
@@ -225,8 +225,8 @@ msgstr "usuario"
msgid "users"
msgstr "usuarios"
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
#: core/models.py:470 core/models.py:1154
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr "título"
@@ -242,128 +242,128 @@ msgstr "Documento"
msgid "Documents"
msgstr "Documentos"
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
#: core/models.py:872
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr "Documento sin título"
#: build/lib/core/models.py:907 core/models.py:907
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "¡{name} ha compartido un documento contigo!"
#: build/lib/core/models.py:911 core/models.py:911
#: build/lib/core/models.py:912 core/models.py:912
#, 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:917 core/models.py:917
#: build/lib/core/models.py:918 core/models.py:918
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha compartido un documento contigo: {title}"
#: build/lib/core/models.py:1015 core/models.py:1015
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr "Traza del enlace de documento/usuario"
#: build/lib/core/models.py:1016 core/models.py:1016
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr "Trazas del enlace de documento/usuario"
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
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:1045 core/models.py:1045
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr "Documento favorito"
#: build/lib/core/models.py:1046 core/models.py:1046
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr "Documentos favoritos"
#: build/lib/core/models.py:1052 core/models.py:1052
#: build/lib/core/models.py:1053 core/models.py:1053
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:1074 core/models.py:1074
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr "Relación documento/usuario"
#: build/lib/core/models.py:1075 core/models.py:1075
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr "Relaciones documento/usuario"
#: build/lib/core/models.py:1081 core/models.py:1081
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr "Este usuario ya forma parte del documento."
#: build/lib/core/models.py:1087 core/models.py:1087
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr "Este equipo ya forma parte del documento."
#: build/lib/core/models.py:1093 build/lib/core/models.py:1241
#: core/models.py:1093 core/models.py:1241
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
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:1155 core/models.py:1155
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr "descripción"
#: build/lib/core/models.py:1156 core/models.py:1156
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr "código"
#: build/lib/core/models.py:1157 core/models.py:1157
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1159 core/models.py:1159
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr "público"
#: build/lib/core/models.py:1161 core/models.py:1161
#: build/lib/core/models.py:1162 core/models.py:1162
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:1167 core/models.py:1167
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr "Plantilla"
#: build/lib/core/models.py:1168 core/models.py:1168
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr "Plantillas"
#: build/lib/core/models.py:1222 core/models.py:1222
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr "Relación plantilla/usuario"
#: build/lib/core/models.py:1223 core/models.py:1223
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr "Relaciones plantilla/usuario"
#: build/lib/core/models.py:1229 core/models.py:1229
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr "Este usuario ya forma parte de la plantilla."
#: build/lib/core/models.py:1235 core/models.py:1235
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr "Este equipo ya se encuentra en esta plantilla."
#: build/lib/core/models.py:1258 core/models.py:1258
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr "dirección de correo electrónico"
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1299 core/models.py:1299
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-04-29 11:48+0000\n"
"PO-Revision-Date: 2025-05-05 09:05\n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -66,32 +66,32 @@ msgstr "Type de corps"
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr "copie de {title}"
#: build/lib/core/enums.py:35 core/enums.py:35
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr "Premier enfant"
#: build/lib/core/enums.py:36 core/enums.py:36
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr "Dernier enfant"
#: build/lib/core/enums.py:37 core/enums.py:37
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr "Premier frère ou sœur"
#: build/lib/core/enums.py:38 core/enums.py:38
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr "Dernière relation"
#: build/lib/core/enums.py:39 core/enums.py:39
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr "Gauche"
#: build/lib/core/enums.py:40 core/enums.py:40
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "Droite"
@@ -225,8 +225,8 @@ msgstr "utilisateur"
msgid "users"
msgstr "utilisateurs"
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
#: core/models.py:470 core/models.py:1154
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr "titre"
@@ -242,128 +242,128 @@ msgstr "Document"
msgid "Documents"
msgstr "Documents"
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
#: core/models.py:872
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr "Document sans titre"
#: build/lib/core/models.py:907 core/models.py:907
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} a partagé un document avec vous!"
#: build/lib/core/models.py:911 core/models.py:911
#: build/lib/core/models.py:912 core/models.py:912
#, 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:917 core/models.py:917
#: build/lib/core/models.py:918 core/models.py:918
#, 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:1015 core/models.py:1015
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr "Trace du lien document/utilisateur"
#: build/lib/core/models.py:1016 core/models.py:1016
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr "Traces du lien document/utilisateur"
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
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:1045 core/models.py:1045
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr "Document favori"
#: build/lib/core/models.py:1046 core/models.py:1046
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr "Documents favoris"
#: build/lib/core/models.py:1052 core/models.py:1052
#: build/lib/core/models.py:1053 core/models.py:1053
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:1074 core/models.py:1074
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr "Relation document/utilisateur"
#: build/lib/core/models.py:1075 core/models.py:1075
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr "Relations document/utilisateur"
#: build/lib/core/models.py:1081 core/models.py:1081
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr "Cet utilisateur est déjà dans ce document."
#: build/lib/core/models.py:1087 core/models.py:1087
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr "Cette équipe est déjà dans ce document."
#: build/lib/core/models.py:1093 build/lib/core/models.py:1241
#: core/models.py:1093 core/models.py:1241
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
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:1155 core/models.py:1155
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr "description"
#: build/lib/core/models.py:1156 core/models.py:1156
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1157 core/models.py:1157
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1159 core/models.py:1159
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr "public"
#: build/lib/core/models.py:1161 core/models.py:1161
#: build/lib/core/models.py:1162 core/models.py:1162
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:1167 core/models.py:1167
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr "Modèle"
#: build/lib/core/models.py:1168 core/models.py:1168
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr "Modèles"
#: build/lib/core/models.py:1222 core/models.py:1222
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr "Relation modèle/utilisateur"
#: build/lib/core/models.py:1223 core/models.py:1223
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr "Relations modèle/utilisateur"
#: build/lib/core/models.py:1229 core/models.py:1229
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr "Cet utilisateur est déjà dans ce modèle."
#: build/lib/core/models.py:1235 core/models.py:1235
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr "Cette équipe est déjà modèle."
#: build/lib/core/models.py:1258 core/models.py:1258
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr "adresse e-mail"
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr "Invitation à un document"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr "Invitations à un document"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1299 core/models.py:1299
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-04-29 11:48+0000\n"
"PO-Revision-Date: 2025-05-05 07:07\n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -66,32 +66,32 @@ msgstr ""
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr "copia di {title}"
#: build/lib/core/enums.py:35 core/enums.py:35
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr ""
#: build/lib/core/enums.py:36 core/enums.py:36
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr ""
#: build/lib/core/enums.py:37 core/enums.py:37
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr ""
#: build/lib/core/enums.py:38 core/enums.py:38
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr ""
#: build/lib/core/enums.py:39 core/enums.py:39
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr "Sinistra"
#: build/lib/core/enums.py:40 core/enums.py:40
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "Destra"
@@ -225,8 +225,8 @@ msgstr "utente"
msgid "users"
msgstr "utenti"
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
#: core/models.py:470 core/models.py:1154
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr "titolo"
@@ -242,128 +242,128 @@ msgstr "Documento"
msgid "Documents"
msgstr "Documenti"
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
#: core/models.py:872
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr "Documento senza titolo"
#: build/lib/core/models.py:907 core/models.py:907
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ha condiviso un documento con te!"
#: build/lib/core/models.py:911 core/models.py:911
#: build/lib/core/models.py:912 core/models.py:912
#, 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:917 core/models.py:917
#: build/lib/core/models.py:918 core/models.py:918
#, 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:1015 core/models.py:1015
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1016 core/models.py:1016
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr "Documento preferito"
#: build/lib/core/models.py:1046 core/models.py:1046
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr "Documenti preferiti"
#: build/lib/core/models.py:1052 core/models.py:1052
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1074 core/models.py:1074
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1075 core/models.py:1075
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1081 core/models.py:1081
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr "Questo utente è già presente in questo documento."
#: build/lib/core/models.py:1087 core/models.py:1087
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr "Questo team è già presente in questo documento."
#: build/lib/core/models.py:1093 build/lib/core/models.py:1241
#: core/models.py:1093 core/models.py:1241
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1155 core/models.py:1155
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr "descrizione"
#: build/lib/core/models.py:1156 core/models.py:1156
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1157 core/models.py:1157
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1159 core/models.py:1159
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr "pubblico"
#: build/lib/core/models.py:1161 core/models.py:1161
#: build/lib/core/models.py:1162 core/models.py:1162
msgid "Whether this template is public for anyone to use."
msgstr "Indica se questo modello è pubblico per chiunque."
#: build/lib/core/models.py:1167 core/models.py:1167
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr "Modello"
#: build/lib/core/models.py:1168 core/models.py:1168
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr "Modelli"
#: build/lib/core/models.py:1222 core/models.py:1222
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1223 core/models.py:1223
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1229 core/models.py:1229
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr "Questo utente è già in questo modello."
#: build/lib/core/models.py:1235 core/models.py:1235
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr "Questo team è già in questo modello."
#: build/lib/core/models.py:1258 core/models.py:1258
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1299 core/models.py:1299
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-04-29 11:48+0000\n"
"PO-Revision-Date: 2025-05-05 07:07\n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -66,32 +66,32 @@ msgstr "Text type"
msgid "Format"
msgstr "Formaat"
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr "kopie van {title}"
#: build/lib/core/enums.py:35 core/enums.py:35
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr "Eerste node"
#: build/lib/core/enums.py:36 core/enums.py:36
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr "Laatste node"
#: build/lib/core/enums.py:37 core/enums.py:37
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr "Eerste naaste"
#: build/lib/core/enums.py:38 core/enums.py:38
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr "Laatste naaste"
#: build/lib/core/enums.py:39 core/enums.py:39
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr "Links"
#: build/lib/core/enums.py:40 core/enums.py:40
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "Rechts"
@@ -225,8 +225,8 @@ msgstr "gebruiker"
msgid "users"
msgstr "gebruikers"
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
#: core/models.py:470 core/models.py:1154
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr "titel"
@@ -242,128 +242,128 @@ msgstr "Document"
msgid "Documents"
msgstr "Documenten"
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
#: core/models.py:872
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr "Naamloos Document"
#: build/lib/core/models.py:907 core/models.py:907
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} heeft een document met gedeeld!"
#: build/lib/core/models.py:911 core/models.py:911
#: build/lib/core/models.py:912 core/models.py:912
#, 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:917 core/models.py:917
#: build/lib/core/models.py:918 core/models.py:918
#, 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:1015 core/models.py:1015
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:1016 core/models.py:1016
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
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:1045 core/models.py:1045
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr "Document favoriet"
#: build/lib/core/models.py:1046 core/models.py:1046
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr "Document favorieten"
#: build/lib/core/models.py:1052 core/models.py:1052
#: build/lib/core/models.py:1053 core/models.py:1053
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:1074 core/models.py:1074
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr "Document/gebruiker relatie"
#: build/lib/core/models.py:1075 core/models.py:1075
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr "Document/gebruiker relaties"
#: build/lib/core/models.py:1081 core/models.py:1081
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr "De gebruiker is al in dit document."
#: build/lib/core/models.py:1087 core/models.py:1087
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr "Het team is al in dit document."
#: build/lib/core/models.py:1093 build/lib/core/models.py:1241
#: core/models.py:1093 core/models.py:1241
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
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:1155 core/models.py:1155
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr "omschrijving"
#: build/lib/core/models.py:1156 core/models.py:1156
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1157 core/models.py:1157
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1159 core/models.py:1159
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr "publiek"
#: build/lib/core/models.py:1161 core/models.py:1161
#: build/lib/core/models.py:1162 core/models.py:1162
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:1167 core/models.py:1167
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr "Template"
#: build/lib/core/models.py:1168 core/models.py:1168
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr "Templates"
#: build/lib/core/models.py:1222 core/models.py:1222
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr "Template/gebruiker relatie"
#: build/lib/core/models.py:1223 core/models.py:1223
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr "Template/gebruiker relaties"
#: build/lib/core/models.py:1229 core/models.py:1229
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr "De gebruiker bestaat al in dit template."
#: build/lib/core/models.py:1235 core/models.py:1235
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr "Het team bestaat al in dit template."
#: build/lib/core/models.py:1258 core/models.py:1258
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr "email adres"
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1299 core/models.py:1299
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-04-29 11:48+0000\n"
"PO-Revision-Date: 2025-05-05 07:07\n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -66,32 +66,32 @@ msgstr ""
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr ""
#: build/lib/core/enums.py:35 core/enums.py:35
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr ""
#: build/lib/core/enums.py:36 core/enums.py:36
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr ""
#: build/lib/core/enums.py:37 core/enums.py:37
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr ""
#: build/lib/core/enums.py:38 core/enums.py:38
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr ""
#: build/lib/core/enums.py:39 core/enums.py:39
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr ""
#: build/lib/core/enums.py:40 core/enums.py:40
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr ""
@@ -225,8 +225,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
#: core/models.py:470 core/models.py:1154
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr ""
@@ -242,128 +242,128 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
#: core/models.py:872
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:907 core/models.py:907
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:911 core/models.py:911
#: build/lib/core/models.py:912 core/models.py:912
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:917 core/models.py:917
#: build/lib/core/models.py:918 core/models.py:918
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1015 core/models.py:1015
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1016 core/models.py:1016
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1046 core/models.py:1046
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1052 core/models.py:1052
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1074 core/models.py:1074
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1075 core/models.py:1075
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1081 core/models.py:1081
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1087 core/models.py:1087
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1093 build/lib/core/models.py:1241
#: core/models.py:1093 core/models.py:1241
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1155 core/models.py:1155
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr ""
#: build/lib/core/models.py:1156 core/models.py:1156
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr ""
#: build/lib/core/models.py:1157 core/models.py:1157
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr ""
#: build/lib/core/models.py:1159 core/models.py:1159
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr ""
#: build/lib/core/models.py:1161 core/models.py:1161
#: build/lib/core/models.py:1162 core/models.py:1162
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1167 core/models.py:1167
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1168 core/models.py:1168
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1222 core/models.py:1222
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1223 core/models.py:1223
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1229 core/models.py:1229
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1235 core/models.py:1235
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1258 core/models.py:1258
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1299 core/models.py:1299
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-04-29 11:48+0000\n"
"PO-Revision-Date: 2025-05-05 07:07\n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -66,32 +66,32 @@ msgstr "Vrsta telesa"
msgid "Format"
msgstr "Oblika"
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr ""
#: build/lib/core/enums.py:35 core/enums.py:35
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr "Prvi otrok"
#: build/lib/core/enums.py:36 core/enums.py:36
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr "Zadnji otrok"
#: build/lib/core/enums.py:37 core/enums.py:37
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr "Prvi brat in sestra"
#: build/lib/core/enums.py:38 core/enums.py:38
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr "Zadnji brat in sestra"
#: build/lib/core/enums.py:39 core/enums.py:39
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr "Levo"
#: build/lib/core/enums.py:40 core/enums.py:40
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "Desno"
@@ -225,8 +225,8 @@ msgstr "uporabnik"
msgid "users"
msgstr "uporabniki"
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
#: core/models.py:470 core/models.py:1154
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr "naslov"
@@ -242,128 +242,128 @@ msgstr "Dokument"
msgid "Documents"
msgstr "Dokumenti"
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
#: core/models.py:872
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr "Dokument brez naslova"
#: build/lib/core/models.py:907 core/models.py:907
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} je delil dokument z vami!"
#: build/lib/core/models.py:911 core/models.py:911
#: build/lib/core/models.py:912 core/models.py:912
#, 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:917 core/models.py:917
#: build/lib/core/models.py:918 core/models.py:918
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} je delil dokument z vami: {title}"
#: build/lib/core/models.py:1015 core/models.py:1015
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr "Dokument/sled povezave uporabnika"
#: build/lib/core/models.py:1016 core/models.py:1016
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr "Sledi povezav dokumenta/uporabnika"
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "A link trace already exists for this document/user."
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr "Priljubljeni dokument"
#: build/lib/core/models.py:1046 core/models.py:1046
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr "Priljubljeni dokumenti"
#: build/lib/core/models.py:1052 core/models.py:1052
#: build/lib/core/models.py:1053 core/models.py:1053
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:1074 core/models.py:1074
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr "Odnos dokument/uporabnik"
#: build/lib/core/models.py:1075 core/models.py:1075
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr "Odnosi dokument/uporabnik"
#: build/lib/core/models.py:1081 core/models.py:1081
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr "Ta uporabnik je že v tem dokumentu."
#: build/lib/core/models.py:1087 core/models.py:1087
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr "Ta ekipa je že v tem dokumentu."
#: build/lib/core/models.py:1093 build/lib/core/models.py:1241
#: core/models.py:1093 core/models.py:1241
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
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:1155 core/models.py:1155
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr "opis"
#: build/lib/core/models.py:1156 core/models.py:1156
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr "koda"
#: build/lib/core/models.py:1157 core/models.py:1157
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1159 core/models.py:1159
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr "javno"
#: build/lib/core/models.py:1161 core/models.py:1161
#: build/lib/core/models.py:1162 core/models.py:1162
msgid "Whether this template is public for anyone to use."
msgstr "Ali je ta predloga javna za uporabo."
#: build/lib/core/models.py:1167 core/models.py:1167
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr "Predloga"
#: build/lib/core/models.py:1168 core/models.py:1168
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr "Predloge"
#: build/lib/core/models.py:1222 core/models.py:1222
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr "Odnos predloga/uporabnik"
#: build/lib/core/models.py:1223 core/models.py:1223
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr "Odnosi med predlogo in uporabnikom"
#: build/lib/core/models.py:1229 core/models.py:1229
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr "Ta uporabnik je že v tej predlogi."
#: build/lib/core/models.py:1235 core/models.py:1235
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr "Ta ekipa je že v tej predlogi."
#: build/lib/core/models.py:1258 core/models.py:1258
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1299 core/models.py:1299
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-04-29 11:48+0000\n"
"PO-Revision-Date: 2025-05-05 07:07\n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -66,32 +66,32 @@ msgstr ""
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr ""
#: build/lib/core/enums.py:35 core/enums.py:35
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr ""
#: build/lib/core/enums.py:36 core/enums.py:36
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr ""
#: build/lib/core/enums.py:37 core/enums.py:37
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr ""
#: build/lib/core/enums.py:38 core/enums.py:38
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr ""
#: build/lib/core/enums.py:39 core/enums.py:39
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr ""
#: build/lib/core/enums.py:40 core/enums.py:40
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr ""
@@ -225,8 +225,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
#: core/models.py:470 core/models.py:1154
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr ""
@@ -242,128 +242,128 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
#: core/models.py:872
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:907 core/models.py:907
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:911 core/models.py:911
#: build/lib/core/models.py:912 core/models.py:912
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:917 core/models.py:917
#: build/lib/core/models.py:918 core/models.py:918
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1015 core/models.py:1015
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1016 core/models.py:1016
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1046 core/models.py:1046
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1052 core/models.py:1052
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1074 core/models.py:1074
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1075 core/models.py:1075
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1081 core/models.py:1081
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1087 core/models.py:1087
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1093 build/lib/core/models.py:1241
#: core/models.py:1093 core/models.py:1241
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1155 core/models.py:1155
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr ""
#: build/lib/core/models.py:1156 core/models.py:1156
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr ""
#: build/lib/core/models.py:1157 core/models.py:1157
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr ""
#: build/lib/core/models.py:1159 core/models.py:1159
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr ""
#: build/lib/core/models.py:1161 core/models.py:1161
#: build/lib/core/models.py:1162 core/models.py:1162
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1167 core/models.py:1167
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1168 core/models.py:1168
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1222 core/models.py:1222
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1223 core/models.py:1223
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1229 core/models.py:1229
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1235 core/models.py:1235
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1258 core/models.py:1258
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1299 core/models.py:1299
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-04-29 11:48+0000\n"
"PO-Revision-Date: 2025-05-05 07:07\n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -66,32 +66,32 @@ msgstr ""
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr ""
#: build/lib/core/enums.py:35 core/enums.py:35
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr ""
#: build/lib/core/enums.py:36 core/enums.py:36
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr ""
#: build/lib/core/enums.py:37 core/enums.py:37
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr ""
#: build/lib/core/enums.py:38 core/enums.py:38
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr ""
#: build/lib/core/enums.py:39 core/enums.py:39
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr ""
#: build/lib/core/enums.py:40 core/enums.py:40
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr ""
@@ -225,8 +225,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
#: core/models.py:470 core/models.py:1154
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr ""
@@ -242,128 +242,128 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
#: core/models.py:872
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:907 core/models.py:907
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:911 core/models.py:911
#: build/lib/core/models.py:912 core/models.py:912
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:917 core/models.py:917
#: build/lib/core/models.py:918 core/models.py:918
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1015 core/models.py:1015
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1016 core/models.py:1016
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1046 core/models.py:1046
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1052 core/models.py:1052
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1074 core/models.py:1074
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1075 core/models.py:1075
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1081 core/models.py:1081
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1087 core/models.py:1087
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1093 build/lib/core/models.py:1241
#: core/models.py:1093 core/models.py:1241
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1155 core/models.py:1155
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr ""
#: build/lib/core/models.py:1156 core/models.py:1156
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr ""
#: build/lib/core/models.py:1157 core/models.py:1157
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr ""
#: build/lib/core/models.py:1159 core/models.py:1159
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr ""
#: build/lib/core/models.py:1161 core/models.py:1161
#: build/lib/core/models.py:1162 core/models.py:1162
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1167 core/models.py:1167
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1168 core/models.py:1168
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1222 core/models.py:1222
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1223 core/models.py:1223
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1229 core/models.py:1229
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1235 core/models.py:1235
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1258 core/models.py:1258
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1299 core/models.py:1299
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-04-29 11:48+0000\n"
"PO-Revision-Date: 2025-05-05 07:07\n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -66,32 +66,32 @@ msgstr "正文类型"
msgid "Format"
msgstr "格式"
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
#: build/lib/core/enums.py:35 core/enums.py:35
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr "第一个子项"
#: build/lib/core/enums.py:36 core/enums.py:36
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr "最后一个子项"
#: build/lib/core/enums.py:37 core/enums.py:37
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr "第一个同级项"
#: build/lib/core/enums.py:38 core/enums.py:38
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr "最后一个同级项"
#: build/lib/core/enums.py:39 core/enums.py:39
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr "左"
#: build/lib/core/enums.py:40 core/enums.py:40
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "右"
@@ -225,8 +225,8 @@ msgstr "用户"
msgid "users"
msgstr "个用户"
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
#: core/models.py:470 core/models.py:1154
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr "标题"
@@ -242,128 +242,128 @@ msgstr "文档"
msgid "Documents"
msgstr "个文档"
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
#: core/models.py:872
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr "未命名文档"
#: build/lib/core/models.py:907 core/models.py:907
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} 与您共享了一个文档!"
#: build/lib/core/models.py:911 core/models.py:911
#: build/lib/core/models.py:912 core/models.py:912
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
#: build/lib/core/models.py:917 core/models.py:917
#: build/lib/core/models.py:918 core/models.py:918
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} 与您共享了一个文档:{title}"
#: build/lib/core/models.py:1015 core/models.py:1015
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr "文档/用户链接跟踪"
#: build/lib/core/models.py:1016 core/models.py:1016
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr "个文档/用户链接跟踪"
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "A link trace already exists for this document/user."
msgstr "此文档/用户的链接跟踪已存在。"
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr "文档收藏"
#: build/lib/core/models.py:1046 core/models.py:1046
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr "文档收藏夹"
#: build/lib/core/models.py:1052 core/models.py:1052
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "该文档已被同一用户的收藏关系实例关联。"
#: build/lib/core/models.py:1074 core/models.py:1074
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr "文档/用户关系"
#: build/lib/core/models.py:1075 core/models.py:1075
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr "文档/用户关系集"
#: build/lib/core/models.py:1081 core/models.py:1081
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr "该用户已在此文档中。"
#: build/lib/core/models.py:1087 core/models.py:1087
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr "该团队已在此文档中。"
#: build/lib/core/models.py:1093 build/lib/core/models.py:1241
#: core/models.py:1093 core/models.py:1241
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
msgid "Either user or team must be set, not both."
msgstr "必须设置用户或团队之一,不能同时设置两者。"
#: build/lib/core/models.py:1155 core/models.py:1155
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr "说明"
#: build/lib/core/models.py:1156 core/models.py:1156
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr "代码"
#: build/lib/core/models.py:1157 core/models.py:1157
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1159 core/models.py:1159
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr "公开"
#: build/lib/core/models.py:1161 core/models.py:1161
#: build/lib/core/models.py:1162 core/models.py:1162
msgid "Whether this template is public for anyone to use."
msgstr "该模板是否公开供任何人使用。"
#: build/lib/core/models.py:1167 core/models.py:1167
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr "模板"
#: build/lib/core/models.py:1168 core/models.py:1168
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr "模板"
#: build/lib/core/models.py:1222 core/models.py:1222
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr "模板/用户关系"
#: build/lib/core/models.py:1223 core/models.py:1223
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr "模板/用户关系集"
#: build/lib/core/models.py:1229 core/models.py:1229
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr "该用户已在此模板中。"
#: build/lib/core/models.py:1235 core/models.py:1235
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr "该团队已在此模板中。"
#: build/lib/core/models.py:1258 core/models.py:1258
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr "电子邮件地址"
#: build/lib/core/models.py:1277 core/models.py:1277
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr "文档邀请"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr "文档邀请"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1299 core/models.py:1299
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.2.1"
version = "3.3.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -26,16 +26,17 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"beautifulsoup4==4.13.4",
"boto3==1.38.13",
"boto3==1.38.23",
"Brotli==1.1.0",
"celery[redis]==5.5.2",
"django-configurations==2.5.1",
"django-cors-headers==4.7.0",
"django-countries==7.6.1",
"django-filter==25.1",
"django-lasuite[all]==0.0.8",
"django-lasuite[all]==0.0.9",
"django-parler==2.3",
"django-redis==5.4.0",
"django-rest-knox==5.0.2",
"django-storages[s3]==1.14.6",
"django-timezone-field>=5.1",
"django==5.1.9",
@@ -51,14 +52,14 @@ dependencies = [
"markdown==3.8",
"mozilla-django-oidc==4.0.1",
"nested-multipart-parser==1.5.0",
"openai==1.78.1",
"psycopg[binary]==3.2.8",
"pycrdt==0.12.15",
"openai==1.82.0",
"psycopg[binary]==3.2.9",
"pycrdt==0.12.20",
"PyJWT==2.10.1",
"python-magic==0.4.27",
"redis<6.0.0",
"requests==2.32.3",
"sentry-sdk==2.28.0",
"sentry-sdk==2.29.1",
"whitenoise==6.9.0",
]
@@ -73,7 +74,7 @@ dev = [
"django-extensions==4.1",
"django-test-migrations==1.5.0",
"drf-spectacular-sidecar==2025.5.1",
"freezegun==1.5.1",
"freezegun==1.5.2",
"ipdb==0.13.13",
"ipython==9.2.0",
"pyfakefs==5.8.0",
@@ -85,8 +86,8 @@ dev = [
"pytest-icdiff==0.9",
"pytest-xdist==3.6.1",
"responses==0.25.7",
"ruff==0.11.9",
"types-requests==2.32.0.20250328",
"ruff==0.11.11",
"types-requests==2.32.0.20250515",
]
[tool.setuptools]

View File

@@ -1,4 +1,9 @@
FROM node:20-alpine AS frontend-deps
FROM node:24-alpine AS frontend-deps
# Upgrade system packages to install security updates
RUN apk update && \
apk upgrade && \
rm -rf /var/cache/apk/*
WORKDIR /home/frontend/
@@ -45,7 +50,7 @@ ENV NEXT_PUBLIC_PUBLISH_AS_MIT=${PUBLISH_AS_MIT}
RUN yarn build
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:1.26-alpine AS frontend-production
FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production
# Un-privileged user running the application
ARG DOCKER_USER

View File

@@ -10,8 +10,6 @@ test.beforeEach(async ({ page }) => {
test.describe('404', () => {
test('Checks all the elements are visible', async ({ page }) => {
await expect(page.getByLabel('Image 404')).toBeVisible();
await expect(page.getByText('Ouch')).toBeVisible();
await expect(
page.getByText(
'It seems that the page you are looking for does not exist or cannot be displayed correctly.',

View File

@@ -1,4 +1,9 @@
<svg width="100" height="100" viewBox="0 0 100 100">
<svg
width="100"
height="100"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="50" cy="30" r="20" fill="#3498db" />
<polygon
points="50,10 55,20 65,20 58,30 60,40 50,35 40,40 42,30 35,20 45,20"

Before

Width:  |  Height:  |  Size: 292 B

After

Width:  |  Height:  |  Size: 336 B

View File

@@ -4,10 +4,11 @@ export const CONFIG = {
AI_FEATURE_ENABLED: true,
CRISP_WEBSITE_ID: null,
COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/',
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY: false,
ENVIRONMENT: 'development',
FRONTEND_CSS_URL: null,
FRONTEND_HOMEPAGE_FEATURE_ENABLED: true,
FRONTEND_THEME: 'default',
FRONTEND_THEME: null,
MEDIA_BASE_URL: 'http://localhost:8083',
LANGUAGES: [
['en-us', 'English'],
@@ -20,7 +21,25 @@ export const CONFIG = {
POSTHOG_KEY: {},
SENTRY_DSN: null,
theme_customization: {},
};
} as const;
export const overrideConfig = async (
page: Page,
newConfig: { [K in keyof typeof CONFIG]?: unknown },
) =>
await page.route('**/api/v1.0/config/', async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...CONFIG,
...newConfig,
},
});
} else {
await route.continue();
}
});
export const keyCloakSignIn = async (
page: Page,
@@ -28,10 +47,7 @@ export const keyCloakSignIn = async (
fromHome: boolean = true,
) => {
if (fromHome) {
await page
.getByRole('button', { name: 'Proconnect Login' })
.first()
.click();
await page.getByRole('button', { name: 'Start Writing' }).first().click();
}
const login = `user-e2e-${browserName}`;
@@ -91,9 +107,16 @@ export const createDoc = async (
};
export const verifyDocName = async (page: Page, docName: string) => {
const input = page.getByRole('textbox', { name: 'doc title input' });
await expect(
page.getByLabel('It is the card information about the document.'),
).toBeVisible({
timeout: 10000,
});
try {
await expect(input).toHaveText(docName);
await expect(
page.getByRole('textbox', { name: 'doc title input' }),
).toHaveText(docName);
} catch {
await expect(page.getByRole('heading', { name: docName })).toBeVisible();
}
@@ -102,7 +125,7 @@ export const verifyDocName = async (page: Page, docName: string) => {
export const addNewMember = async (
page: Page,
index: number,
role: 'Administrator' | 'Owner' | 'Member' | 'Editor' | 'Reader',
role: 'Administrator' | 'Owner' | 'Editor' | 'Reader',
fillText: string = 'user ',
) => {
const responsePromiseSearchUser = page.waitForResponse(

View File

@@ -2,38 +2,14 @@ import path from 'path';
import { expect, test } from '@playwright/test';
import { CONFIG, createDoc } from './common';
import { CONFIG, createDoc, overrideConfig } from './common';
test.describe('Config', () => {
test('it checks the config api is called', async ({ page }) => {
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes('/config/') && response.status() === 200,
);
await page.goto('/');
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
expect(await response.json()).toStrictEqual(CONFIG);
});
test('it checks that sentry is trying to init from config endpoint', async ({
page,
}) => {
await page.route('**/api/v1.0/config/', async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...CONFIG,
SENTRY_DSN: 'https://sentry.io/123',
},
});
} else {
await route.continue();
}
await overrideConfig(page, {
SENTRY_DSN: 'https://sentry.io/123',
});
const invalidMsg = 'Invalid Sentry Dsn: https://sentry.io/123';
@@ -98,18 +74,8 @@ test.describe('Config', () => {
page,
browserName,
}) => {
await page.route('**/api/v1.0/config/', async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...CONFIG,
AI_FEATURE_ENABLED: false,
},
});
} else {
await route.continue();
}
await overrideConfig(page, {
AI_FEATURE_ENABLED: false,
});
await page.goto('/');
@@ -129,18 +95,8 @@ test.describe('Config', () => {
test('it checks that Crisp is trying to init from config endpoint', async ({
page,
}) => {
await page.route('**/api/v1.0/config/', async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...CONFIG,
CRISP_WEBSITE_ID: '1234',
},
});
} else {
await route.continue();
}
await overrideConfig(page, {
CRISP_WEBSITE_ID: '1234',
});
await page.goto('/');
@@ -151,18 +107,8 @@ test.describe('Config', () => {
});
test('it checks FRONTEND_CSS_URL config', async ({ page }) => {
await page.route('**/api/v1.0/config/', async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...CONFIG,
FRONTEND_CSS_URL: 'http://localhost:123465/css/style.css',
},
});
} else {
await route.continue();
}
await overrideConfig(page, {
FRONTEND_CSS_URL: 'http://localhost:123465/css/style.css',
});
await page.goto('/');
@@ -178,9 +124,7 @@ test.describe('Config', () => {
test.describe('Config: Not loggued', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('it checks that theme is configured from config endpoint', async ({
page,
}) => {
test('it checks the config api is called', async ({ page }) => {
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes('/config/') && response.status() === 200,
@@ -191,11 +135,25 @@ test.describe('Config: Not loggued', () => {
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
const jsonResponse = await response.json();
expect(jsonResponse.FRONTEND_THEME).toStrictEqual('default');
const json = (await response.json()) as typeof CONFIG;
const { theme_customization, ...configApi } = json;
expect(theme_customization).toBeDefined();
const { theme_customization: _, ...CONFIG_LEFT } = CONFIG;
const footer = page.locator('footer').first();
expect(configApi).toStrictEqual(CONFIG_LEFT);
});
test('it checks that theme is configured from config endpoint', async ({
page,
}) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
});
await page.goto('/');
const header = page.locator('header').first();
// alt 'Gouvernement Logo' comes from the theme
await expect(footer.getByAltText('Gouvernement Logo')).toBeVisible();
await expect(header.getByAltText('Gouvernement Logo')).toBeVisible();
});
});

View File

@@ -4,6 +4,8 @@ import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
import {
CONFIG,
addNewMember,
createDoc,
goToGridDoc,
mockedDocument,
@@ -363,7 +365,7 @@ test.describe('Doc Editor', () => {
partial_update: true,
retrieve: true,
},
link_reach: 'public',
link_reach: 'restricted',
link_role: 'editor',
created_at: '2021-09-01T09:00:00Z',
title: '',
@@ -423,6 +425,10 @@ test.describe('Doc Editor', () => {
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`html`);
});
const responseCheckPromise = page.waitForResponse(
(response) =>
response.url().includes('media-check') && response.status() === 200,
);
await verifyDocName(page, randomDoc);
@@ -437,6 +443,8 @@ test.describe('Doc Editor', () => {
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.html'));
await responseCheckPromise;
await page.locator('.bn-block-content[data-name="test.html"]').click();
await page.getByRole('button', { name: 'Download file' }).click();
@@ -453,6 +461,101 @@ test.describe('Doc Editor', () => {
expect(svgBuffer.toString()).toContain('Hello svg');
});
test('it analyzes uploads', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
let requestCount = 0;
await page.route(
/.*\/documents\/.*\/media-check\/\?key=.*/,
async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
status: requestCount ? 'ready' : 'processing',
file: '/anything.html',
},
});
requestCount++;
} else {
await route.continue();
}
},
);
const fileChooserPromise = page.waitForEvent('filechooser');
await verifyDocName(page, randomDoc);
const editor = page.locator('.ProseMirror.bn-editor');
await editor.click();
await editor.locator('.bn-block-outer').last().fill('/');
await page.getByText('Embedded file').click();
await page.getByText('Upload file').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.html'));
await expect(editor.getByText('Analyzing file...')).toBeVisible();
// The retry takes a few seconds
await expect(editor.getByText('test.html')).toBeVisible({
timeout: 7000,
});
await expect(editor.getByText('Analyzing file...')).toBeHidden();
});
test('it checks block editing when not connected to collab server', async ({
page,
}) => {
await page.route('**/api/v1.0/config/', async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...CONFIG,
COLLABORATION_WS_URL: 'ws://localhost:5555/collaboration/ws/',
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY: true,
},
});
} else {
await route.continue();
}
});
await page.goto('/');
void page
.getByRole('button', {
name: 'New doc',
})
.click();
const card = page.getByLabel('It is the card information');
await expect(
card.getByText('Your network do not allow you to edit'),
).toBeHidden();
const editor = page.locator('.ProseMirror');
await expect(editor).toHaveAttribute('contenteditable', 'true');
await page.getByRole('button', { name: 'Share' }).click();
await addNewMember(page, 0, 'Editor', 'impress');
// Close the modal
await page.getByRole('button', { name: 'close' }).first().click();
await expect(
card.getByText('Your network do not allow you to edit'),
).toBeVisible({
timeout: 10000,
});
await expect(editor).toHaveAttribute('contenteditable', 'false');
});
test('it checks if callout custom block', async ({ page, browserName }) => {
await createDoc(page, 'doc-toolbar', browserName, 1);

View File

@@ -208,7 +208,6 @@ test.describe('Documents filters', () => {
// Initial state
await expect(allDocs).toBeVisible();
await expect(allDocs).toHaveCSS('background-color', 'rgb(238, 238, 238)');
await expect(allDocs).toHaveAttribute('aria-selected', 'true');
await expect(myDocs).toBeVisible();

View File

@@ -101,6 +101,8 @@ test.describe('Doc Visibility: Restricted', () => {
page,
browserName,
}) => {
test.slow();
await page.goto('/');
await keyCloakSignIn(page, browserName);
@@ -121,14 +123,16 @@ test.describe('Doc Visibility: Restricted', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible();
await page.goto(urlDoc);
await expect(
page.getByText('You do not have permission to view this document.'),
).toBeVisible();
).toBeVisible({
timeout: 10000,
});
});
test('A doc is accessible when member.', async ({ page, browserName }) => {
@@ -173,7 +177,7 @@ test.describe('Doc Visibility: Restricted', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible();
await page.goto(urlDoc);
@@ -430,7 +434,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible();
await page.goto(urlDoc);
@@ -490,7 +494,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible();
await page.goto(urlDoc);

View File

@@ -1,14 +1,115 @@
import { expect, test } from '@playwright/test';
import { overrideConfig } from './common';
test.describe('Footer', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('checks the footer is not displayed if no config', async ({ page }) => {
await overrideConfig(page, {
theme_customization: {},
});
await page.goto('/');
await expect(page.locator('footer')).toBeHidden();
});
test('checks all the elements are visible', async ({ page }) => {
await page.goto('/');
const footer = page.locator('footer').first();
await expect(footer.getByAltText('Docs Logo')).toBeVisible();
await expect(footer.getByRole('heading', { name: 'Docs' })).toBeVisible();
await expect(footer.getByRole('link', { name: 'Github' })).toBeVisible();
await expect(footer.getByRole('link', { name: 'DINUM' })).toBeVisible();
await expect(footer.getByRole('link', { name: 'ZenDiS' })).toBeVisible();
await expect(
footer.getByRole('link', { name: 'BlockNote.js' }),
).toBeVisible();
await expect(
footer.getByRole('link', { name: 'Legal Notice' }),
).toBeVisible();
await expect(
footer.getByRole('link', { name: 'Personal data and cookies' }),
).toBeVisible();
await expect(
footer.getByRole('link', { name: 'Accessibility' }),
).toBeVisible();
await expect(
footer.getByText(
'Unless otherwise stated, all content on this site is under licence',
),
).toBeVisible();
// Check the translation
const header = page.locator('header').first();
await header.getByRole('button').getByText('English').click();
await page.getByLabel('Français').click();
await expect(
page.locator('footer').getByText('Mentions légales'),
).toBeVisible();
});
test('checks the footer is correctly overrided', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
theme_customization: {
footer: {
default: {
logo: {
src: '/assets/logo-gouv.svg',
width: '220px',
alt: 'Gouvernement Logo',
},
externalLinks: [
{
label: 'legifrance.gouv.fr',
href: '#',
},
{
label: 'info.gouv.fr',
href: '#',
},
],
legalLinks: [
{
label: 'Legal link',
href: '#',
},
],
bottomInformation: {
label: 'Some bottom information text',
link: {
label: 'a custom label',
href: '#',
},
},
},
fr: {
bottomInformation: {
label: "Text d'information en bas de page en français",
link: {
label: 'un label personnalisé',
href: '#',
},
},
},
},
},
});
await page.goto('/');
const footer = page.locator('footer').first();
await expect(footer.getByAltText('Gouvernement Logo')).toBeVisible();
await expect(footer.getByRole('heading', { name: 'Docs' })).toBeHidden();
await expect(footer.getByText('BETA')).toBeHidden();
await expect(
footer.getByRole('link', { name: 'legifrance.gouv.fr' }),
).toBeVisible();
@@ -18,53 +119,30 @@ test.describe('Footer', () => {
).toBeVisible();
await expect(
footer.getByRole('link', { name: 'service-public.fr' }),
footer.getByRole('link', { name: 'Legal link' }),
).toBeVisible();
await expect(
footer.getByRole('link', { name: 'data.gouv.fr' }),
footer.getByText('Some bottom information text'),
).toBeVisible();
await expect(
footer.getByRole('link', { name: 'Legal Notice' }),
footer.getByRole('link', { name: 'a custom label' }),
).toBeVisible();
// Check the translation
const header = page.locator('header').first();
await header.getByRole('button').getByText('English').click();
await page.getByLabel('Français').click();
await expect(
page
.locator('footer')
.getByText("Text d'information en bas de page en français"),
).toBeVisible();
await expect(
footer.getByRole('link', { name: 'Personal data and cookies' }),
).toBeVisible();
await expect(
footer.getByRole('link', { name: 'Accessibility' }),
).toBeVisible();
await expect(
footer.getByText(
'Unless otherwise stated, all content on this site is under licence',
),
footer.getByRole('link', { name: 'un label personnalisé' }),
).toBeVisible();
});
const legalPages = [
{ name: 'Legal Notice', url: '/legal-notice/' },
{ name: 'Personal data and cookies', url: '/personal-data-cookies/' },
{ name: 'Accessibility', url: '/accessibility/' },
];
for (const { name, url } of legalPages) {
test(`checks ${name} page`, async ({ page }) => {
await page.goto('/');
const footer = page.locator('footer').first();
await footer.getByRole('link', { name }).click();
await expect(
page
.getByRole('heading', {
name,
})
.first(),
).toBeVisible();
await expect(page).toHaveURL(url);
});
}
});

View File

@@ -1,20 +1,39 @@
import { expect, test } from '@playwright/test';
import { expectLoginPage, keyCloakSignIn } from './common';
import { expectLoginPage, keyCloakSignIn, overrideConfig } from './common';
test.describe('Header', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('checks all the elements are visible', async ({ page }) => {
await page.goto('/');
const header = page.locator('header').first();
await expect(header.getByLabel('Docs Logo')).toBeVisible();
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
'color',
'rgb(0, 0, 145)',
'font-family',
/Roboto/i,
);
await expect(
header.getByRole('button', {
name: 'Logout',
}),
).toBeVisible();
await expect(header.getByText('English')).toBeVisible();
});
test('checks all the elements are visible with DSFR theme', async ({
page,
}) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
});
await page.goto('/');
const header = page.locator('header').first();
await expect(header.getByLabel('Docs Logo')).toBeVisible();
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
'font-family',
/Marianne/i,
@@ -36,6 +55,11 @@ test.describe('Header', () => {
});
test('checks La Gauffre interaction', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
});
await page.goto('/');
const header = page.locator('header').first();
await expect(
@@ -68,11 +92,13 @@ test.describe('Header', () => {
test.describe('Header mobile', () => {
test.use({ viewport: { width: 500, height: 1200 } });
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('it checks the header when mobile with DSFR theme', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
});
await page.goto('/');
test('it checks the header when mobile', async ({ page }) => {
const header = page.locator('header').first();
await expect(header.getByLabel('Open the header menu')).toBeVisible();

View File

@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { CONFIG } from './common';
import { overrideConfig } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/docs/');
@@ -9,6 +9,62 @@ test.beforeEach(async ({ page }) => {
test.describe('Home page', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('checks all the elements are visible', async ({ page }) => {
await page.goto('/docs/');
// 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();
await expect(header.getByRole('img', { name: 'Docs logo' })).toBeVisible();
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
// Check the titles
const h2 = page.locator('h2');
await expect(h2.getByText('Govs ❤️ Open Source.')).toBeVisible();
await expect(
h2.getByText('Collaborative writing, Simplified.'),
).toBeVisible();
await expect(
h2.getByText('An uncompromising writing experience.'),
).toBeVisible();
await expect(
h2.getByText('Simple and secure collaboration.'),
).toBeVisible();
await expect(h2.getByText('Flexible export.')).toBeVisible();
await expect(
h2.getByText('A new way to organize knowledge.'),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Start Writing' }),
).toBeVisible();
await expect(footer).toBeVisible();
});
test('checks all the elements are visible with dsfr theme', async ({
page,
}) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
theme_customization: {
footer: {
default: {
externalLinks: [
{
label: 'legifrance.gouv.fr',
href: '#',
},
],
},
},
},
});
await page.goto('/docs/');
// Check header content
const header = page.locator('header').first();
const footer = page.locator('footer').first();
@@ -54,18 +110,8 @@ test.describe('Home page', () => {
});
test('it checks the homepage feature flag', async ({ page }) => {
await page.route('**/api/v1.0/config/', async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...CONFIG,
FRONTEND_HOMEPAGE_FEATURE_ENABLED: false,
},
});
} else {
await route.continue();
}
await overrideConfig(page, {
FRONTEND_HOMEPAGE_FEATURE_ENABLED: false,
});
await page.goto('/');

View File

@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "3.2.1",
"version": "3.3.0",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",

View File

@@ -3,6 +3,8 @@ import { cunninghamConfig as tokens } from '@gouvfr-lasuite/ui-kit';
const customColors = {
'primary-action': '#1212FF',
'primary-bg': '#FAFAFA',
'primary-focus': '#0A76F6',
'secondary-icon': 'var(--c--theme--colors--primary-text)',
'blue-400': '#7AB1E8',
'blue-500': '#417DC4',
'blue-600': '#3558A2',
@@ -35,21 +37,220 @@ const customColors = {
'yellow-600': '#66673D',
};
tokens.themes.default.theme.colors = {
...tokens.themes.default.theme.colors,
...customColors,
tokens.themes.default.theme = {
...tokens.themes.default.theme,
...{
logo: {
src: '',
alt: '',
widthHeader: '',
widthFooter: '',
},
colors: {
...tokens.themes.default.theme.colors,
...customColors,
},
},
};
tokens.themes.default.components = {
...tokens.themes.default.components,
...{
'la-gauffre': {
activated: true,
},
'home-proconnect': {
activated: true,
'la-gaufre': false,
'home-proconnect': false,
beta: false,
'image-system-filter': '',
favicon: {
ico: '/assets/favicon-light.ico',
'png-light': '/assets/favicon-light.png',
'png-dark': '/assets/favicon-dark.png',
},
},
};
export default tokens;
const dsfrTheme = {
dsfr: {
theme: {
colors: {
'secondary-icon': '#C9191E',
},
logo: {
src: '/assets/logo-gouv.svg',
widthHeader: '110px',
widthFooter: '220px',
alt: 'Gouvernement Logo',
},
},
components: {
'la-gaufre': true,
'home-proconnect': true,
beta: true,
favicon: {
ico: '/assets/favicon-dsfr.ico',
'png-light': '/assets/favicon-dsfr.png',
'png-dark': '/assets/favicon-dark-dsfr.png',
},
},
},
};
const genericTheme = {
generic: {
theme: {
colors: {
'primary-action': '#206EBD',
'primary-focus': '#1E64BF',
'primary-text': '#2E2C28',
'primary-050': '#F8F8F7',
'primary-100': '#F0EFEC',
'primary-150': '#F4F4FD',
'primary-200': '#E8E7E4',
'primary-300': '#CFCDC9',
'primary-400': '#979592',
'primary-500': '#82807D',
'primary-600': '#3F3D39',
'primary-700': '#2E2C28',
'primary-800': '#302E29',
'primary-900': '#282622',
'primary-950': '#201F1C',
'secondary-text': '#fff',
'secondary-50': '#F4F7FA',
'secondary-100': '#D7E3EE',
'secondary-200': '#B8CCE1',
'secondary-300': '#99B4D3',
'secondary-400': '#7595BE',
'secondary-500': '#5874A0',
'secondary-600': '#3A5383',
'secondary-700': '#1E3462',
'secondary-800': '#091B41',
'secondary-900': '#08183B',
'secondary-950': '#071636',
'greyscale-text': '#3C3B38',
'greyscale-000': '#fff',
'greyscale-050': '#F8F7F7',
'greyscale-100': '#F3F3F2',
'greyscale-200': '#ECEBEA',
'greyscale-250': '#E4E3E2',
'greyscale-300': '#D3D2CF',
'greyscale-350': '#eee',
'greyscale-400': '#96948E',
'greyscale-500': '#817E77',
'greyscale-600': '#6A6862',
'greyscale-700': '#3C3B38',
'greyscale-750': '#383632',
'greyscale-800': '#2D2B27',
'greyscale-900': '#262522',
'greyscale-950': '#201F1C',
'greyscale-1000': '#181714',
'success-text': '#234935',
'success-50': '#F3FBF5',
'success-100': '#E4F7EA',
'success-200': '#CAEED4',
'success-300': '#A0E0B5',
'success-400': '#6CC88C',
'success-500': '#6CC88C',
'success-600': '#358D5C',
'success-700': '#2D704B',
'success-800': '#28583F',
'success-900': '#234935',
'success-950': '#0F281B',
'info-text': '#212445',
'info-50': '#F2F6FB',
'info-100': '#E2E9F5',
'info-200': '#CCD8EE',
'info-300': '#A9C0E3',
'info-400': '#809DD4',
'info-500': '#617BC7',
'info-600': '#4A5CBF',
'info-700': '#3E49B2',
'info-800': '#353C8F',
'info-900': '#303771',
'info-950': '#212445',
'warning-text': '#D97C3A',
'warning-50': '#FDF7F1',
'warning-100': '#FBEDDC',
'warning-200': '#F5D9B9',
'warning-300': '#EDBE8C',
'warning-400': '#E2985C',
'warning-500': '#D97C3A',
'warning-600': '#C96330',
'warning-700': '#A34B32',
'warning-800': '#813B2C',
'warning-900': '#693327',
'warning-950': '#381713',
'danger-action': '#C0182A',
'danger-text': '#FFF',
'danger-050': '#FDF5F4',
'danger-100': '#FBEBE8',
'danger-200': '#F9E0DC',
'danger-300': '#F3C3BD',
'danger-400': '#E26552',
'danger-500': '#C91F00',
'danger-600': '#A71901',
'danger-700': '#562C2B',
'danger-800': '#392425',
'danger-900': '#311F20',
'danger-950': '#2A191A',
'blue-400': '#8BAECC',
'blue-500': '#567AA2',
'blue-600': '#455784',
'brown-400': '#E4C090',
'brown-500': '#BA9977',
'brown-600': '#735C45',
'cyan-400': '#5CBEC9',
'cyan-500': '#43A1B3',
'cyan-600': '#39809B',
'gold-400': '#ECBF50',
'gold-500': '#DFA038',
'gold-600': '#C17B31',
'green-400': '#5DBD9A',
'green-500': '#3AA183',
'green-600': '#2A816D',
'olive-400': '#AFD662',
'olive-500': '#90BB4B',
'olive-600': '#6E9441',
'orange-400': '#E2985C',
'orange-500': '#D97C3A',
'orange-600': '#C96330',
'pink-400': '#BE8FC8',
'pink-500': '#A563B1',
'pink-600': '#8B44A5',
'purple-400': '#BE8FC8',
'purple-500': '#A563B1',
'purple-600': '#8B44A5',
'yellow-400': '#EDC947',
'yellow-500': '#DBB13A',
'yellow-600': '#B88A34',
},
font: {
families: {
base: 'Inter, Roboto Flex Variable, sans-serif',
accent: 'Inter, Roboto Flex Variable, sans-serif',
},
},
},
components: {
button: {
primary: {
background: {
'color-hover': 'var(--c--theme--colors--primary-focus)',
'color-active': 'var(--c--theme--colors--primary-focus)',
'color-focus': 'var(--c--theme--colors--primary-focus)',
},
},
},
'image-system-filter': 'saturate(0.2)',
},
},
};
const docsTokens = {
...tokens,
themes: {
...tokens.themes,
...dsfrTheme,
...genericTheme,
},
};
export default docsTokens;

View File

@@ -1,12 +1,12 @@
{
"name": "app-impress",
"version": "3.2.1",
"version": "3.3.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "prettier --check . && yarn stylelint && next build",
"build:ci": "cp .env.development .env.local && yarn build",
"build-theme": "cunningham -g css,ts -o src/cunningham --utility-classes && yarn prettier",
"build-theme": "cunningham -g css,ts -o src/cunningham --utility-classes && yarn prettier && yarn stylelint --fix",
"start": "npx -y serve@latest out",
"lint": "tsc --noEmit && next lint",
"prettier": "prettier --write .",
@@ -15,57 +15,57 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@ag-media/react-pdf-table": "2.0.2",
"@blocknote/code-block": "0.29.1",
"@blocknote/core": "0.29.1",
"@blocknote/mantine": "0.29.1",
"@blocknote/react": "0.29.1",
"@blocknote/xl-docx-exporter": "0.29.1",
"@blocknote/xl-pdf-exporter": "0.29.1",
"@ag-media/react-pdf-table": "2.0.3",
"@blocknote/code-block": "0.31.1",
"@blocknote/core": "0.31.1",
"@blocknote/mantine": "0.31.1",
"@blocknote/react": "0.31.1",
"@blocknote/xl-docx-exporter": "0.31.1",
"@blocknote/xl-pdf-exporter": "0.31.1",
"@emoji-mart/data": "1.2.1",
"@emoji-mart/react": "1.1.1",
"@fontsource/material-icons": "5.2.5",
"@gouvfr-lasuite/integration": "1.0.3",
"@gouvfr-lasuite/ui-kit": "0.4.1",
"@gouvfr-lasuite/ui-kit": "0.7.0",
"@hocuspocus/provider": "2.15.2",
"@openfun/cunningham-react": "3.0.0",
"@openfun/cunningham-react": "3.1.0",
"@react-pdf/renderer": "4.3.0",
"@sentry/nextjs": "9.15.0",
"@tanstack/react-query": "5.75.4",
"@sentry/nextjs": "9.22.0",
"@tanstack/react-query": "5.77.1",
"canvg": "4.0.3",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"crisp-sdk-web": "1.0.25",
"docx": "9.4.1",
"docx": "9.5.0",
"emoji-mart": "5.6.0",
"i18next": "25.1.1",
"i18next": "25.2.1",
"i18next-browser-languagedetector": "8.1.0",
"idb": "8.0.2",
"idb": "8.0.3",
"lodash": "4.17.21",
"luxon": "3.6.1",
"next": "15.3.1",
"posthog-js": "1.239.1",
"next": "15.3.2",
"posthog-js": "1.246.0",
"react": "*",
"react-aria-components": "1.8.0",
"react-aria-components": "1.9.0",
"react-dom": "*",
"react-i18next": "15.5.1",
"react-i18next": "15.5.2",
"react-intersection-observer": "9.16.0",
"react-select": "5.10.1",
"styled-components": "6.1.17",
"styled-components": "6.1.18",
"use-debounce": "10.0.4",
"y-protocols": "1.0.6",
"yjs": "*",
"zustand": "5.0.4"
"zustand": "5.0.5"
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.75.4",
"@tanstack/react-query-devtools": "5.77.1",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.3.0",
"@testing-library/user-event": "14.6.1",
"@types/jest": "29.5.14",
"@types/lodash": "4.17.16",
"@types/lodash": "4.17.17",
"@types/luxon": "3.6.2",
"@types/node": "*",
"@types/react": "*",
@@ -82,7 +82,7 @@
"stylelint-config-standard": "38.0.0",
"stylelint-prettier": "5.0.3",
"typescript": "*",
"webpack": "5.99.7",
"webpack": "5.99.9",
"workbox-webpack-plugin": "7.1.0"
}
}

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 992 B

After

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,12 @@
<svg viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M21.6305 29.5812C22.7983 29.2538 23.9166 28.6562 24.6505 27.6003C25.3749 26.5663 25.5789 25.2547 25.5789 23.9925V5.50099C25.5789 5.17358 25.5611 4.84557 25.5216 4.52148C26.1016 4.74961 26.5486 5.12658 26.8626 5.65239C27.2331 6.25024 27.4184 7.03757 27.4184 8.01435V26.7964C27.4184 28.1184 27.0942 29.1078 26.4458 29.7646C25.7974 30.4214 24.8207 30.7498 23.5155 30.7498H16.4209C16.5889 30.7204 16.7574 30.6901 16.9262 30.659C18.4067 30.3944 19.9713 30.0354 21.6185 29.5846L21.6305 29.5812Z"
fill="#C9191E"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.58203 26.405V7.5977C4.58203 6.45251 4.88938 5.58519 5.50408 4.99575C6.1272 4.40631 6.95242 4.08212 7.97972 4.02318C9.49542 3.93055 10.9311 3.80425 12.2868 3.64425C13.6425 3.47584 14.9393 3.28217 16.1771 3.06324C17.4234 2.8443 18.6359 2.60011 19.8148 2.33065C21.0274 2.04435 21.9578 2.1875 22.6062 2.7601C23.2546 3.33269 23.5788 4.24632 23.5788 5.50099V23.9925C23.5788 25.0956 23.3893 25.9166 23.0104 26.4555C22.6315 27.0029 21.9915 27.4028 21.0905 27.6554C19.4906 28.0933 17.9833 28.4386 16.5687 28.6912C15.154 28.9522 13.7731 29.1501 12.4258 29.2848C11.0785 29.4196 9.69751 29.5248 8.28286 29.6006C7.11241 29.668 6.20299 29.4238 5.5546 28.868C4.90622 28.3207 4.58203 27.4997 4.58203 26.405ZM9.20865 11.0124C11.0635 10.8944 12.7632 10.7131 14.3075 10.4683C14.6822 10.4072 15.0564 10.3436 15.4291 10.2776C15.8192 10.2085 16.1013 9.86859 16.1013 9.47337C16.1013 8.96154 15.638 8.57609 15.135 8.66189C14.846 8.71118 14.5555 8.75909 14.2635 8.80562C12.7346 9.04923 11.0452 9.22998 9.19523 9.3477C8.91819 9.36558 8.69776 9.45188 8.55608 9.62391C8.42209 9.78661 8.35645 9.98229 8.35645 10.2053C8.35645 10.4321 8.43296 10.6295 8.58568 10.7918L8.58783 10.7939C8.75336 10.9595 8.96369 11.0311 9.20865 11.0124ZM9.20801 15.206C11.0631 15.088 12.763 14.9066 14.3075 14.6619C15.8588 14.4089 17.3936 14.1138 18.9112 13.7766C19.2191 13.7081 19.4498 13.6003 19.5652 13.433C19.6786 13.2721 19.7347 13.0876 19.7347 12.8832C19.7347 12.6526 19.6469 12.454 19.476 12.2926C19.2921 12.1189 19.0348 12.0784 18.7304 12.1411L18.7285 12.1415C17.2823 12.4694 15.794 12.7553 14.2635 12.9992C12.7346 13.2428 11.0452 13.4235 9.19523 13.5413C8.91819 13.5591 8.69776 13.6454 8.55608 13.8175C8.42276 13.9794 8.35645 14.1705 8.35645 14.3863C8.35645 14.6203 8.43209 14.8223 8.58558 14.9854L8.59 14.9896C8.75499 15.1449 8.96316 15.2155 9.20551 15.2062L9.20801 15.206ZM9.20847 19.3994C11.0634 19.2729 12.7631 19.0874 14.3075 18.8427C15.8589 18.5982 17.3934 18.3073 18.9112 17.97C19.2199 17.9014 19.4508 17.7891 19.566 17.6127C19.6783 17.4529 19.7347 17.2733 19.7347 17.0766C19.7347 16.8461 19.6469 16.6474 19.476 16.4861C19.2921 16.3123 19.0348 16.2718 18.7304 16.3345L18.729 16.3348C17.2827 16.6543 15.7942 16.9361 14.2635 17.18C12.7345 17.4236 11.045 17.6086 9.19495 17.7347C8.91804 17.7526 8.69771 17.8389 8.55608 18.0109C8.42276 18.1728 8.35645 18.3639 8.35645 18.5797C8.35645 18.8137 8.43209 19.0158 8.58558 19.1789L8.59 19.183C8.75499 19.3383 8.96316 19.4089 9.20551 19.3996L9.20847 19.3994ZM14.3075 23.007C12.7632 23.2518 11.0635 23.4331 9.20867 23.5512C8.9637 23.5698 8.75337 23.4982 8.58783 23.3326L8.58572 23.3305C8.433 23.1682 8.35645 22.9708 8.35645 22.7441C8.35645 22.521 8.42209 22.3253 8.55608 22.1626C8.69776 21.9906 8.91827 21.9043 9.19531 21.8864C11.0453 21.7687 12.7346 21.588 14.2635 21.3443C14.5555 21.2978 14.846 21.2499 15.135 21.2006C15.638 21.1148 16.1013 21.5003 16.1013 22.0121C16.1013 22.4073 15.8192 22.7472 15.4291 22.8163C15.0564 22.8823 14.6822 22.9459 14.3075 23.007Z"
fill="#000091"
/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1,12 +1,12 @@
<svg viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M21.6305 29.5812C22.7983 29.2538 23.9166 28.6562 24.6505 27.6003C25.3749 26.5663 25.5789 25.2547 25.5789 23.9925V5.50099C25.5789 5.17358 25.5611 4.84557 25.5216 4.52148C26.1016 4.74961 26.5486 5.12658 26.8626 5.65239C27.2331 6.25024 27.4184 7.03757 27.4184 8.01435V26.7964C27.4184 28.1184 27.0942 29.1078 26.4458 29.7646C25.7974 30.4214 24.8207 30.7498 23.5155 30.7498H16.4209C16.5889 30.7204 16.7574 30.6901 16.9262 30.659C18.4067 30.3944 19.9713 30.0354 21.6185 29.5846L21.6305 29.5812Z"
fill="#C9191E"
fill="var(--c--theme--colors--secondary-icon)"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.58203 26.405V7.5977C4.58203 6.45251 4.88938 5.58519 5.50408 4.99575C6.1272 4.40631 6.95242 4.08212 7.97972 4.02318C9.49542 3.93055 10.9311 3.80425 12.2868 3.64425C13.6425 3.47584 14.9393 3.28217 16.1771 3.06324C17.4234 2.8443 18.6359 2.60011 19.8148 2.33065C21.0274 2.04435 21.9578 2.1875 22.6062 2.7601C23.2546 3.33269 23.5788 4.24632 23.5788 5.50099V23.9925C23.5788 25.0956 23.3893 25.9166 23.0104 26.4555C22.6315 27.0029 21.9915 27.4028 21.0905 27.6554C19.4906 28.0933 17.9833 28.4386 16.5687 28.6912C15.154 28.9522 13.7731 29.1501 12.4258 29.2848C11.0785 29.4196 9.69751 29.5248 8.28286 29.6006C7.11241 29.668 6.20299 29.4238 5.5546 28.868C4.90622 28.3207 4.58203 27.4997 4.58203 26.405ZM9.20865 11.0124C11.0635 10.8944 12.7632 10.7131 14.3075 10.4683C14.6822 10.4072 15.0564 10.3436 15.4291 10.2776C15.8192 10.2085 16.1013 9.86859 16.1013 9.47337C16.1013 8.96154 15.638 8.57609 15.135 8.66189C14.846 8.71118 14.5555 8.75909 14.2635 8.80562C12.7346 9.04923 11.0452 9.22998 9.19523 9.3477C8.91819 9.36558 8.69776 9.45188 8.55608 9.62391C8.42209 9.78661 8.35645 9.98229 8.35645 10.2053C8.35645 10.4321 8.43296 10.6295 8.58568 10.7918L8.58783 10.7939C8.75336 10.9595 8.96369 11.0311 9.20865 11.0124ZM9.20801 15.206C11.0631 15.088 12.763 14.9066 14.3075 14.6619C15.8588 14.4089 17.3936 14.1138 18.9112 13.7766C19.2191 13.7081 19.4498 13.6003 19.5652 13.433C19.6786 13.2721 19.7347 13.0876 19.7347 12.8832C19.7347 12.6526 19.6469 12.454 19.476 12.2926C19.2921 12.1189 19.0348 12.0784 18.7304 12.1411L18.7285 12.1415C17.2823 12.4694 15.794 12.7553 14.2635 12.9992C12.7346 13.2428 11.0452 13.4235 9.19523 13.5413C8.91819 13.5591 8.69776 13.6454 8.55608 13.8175C8.42276 13.9794 8.35645 14.1705 8.35645 14.3863C8.35645 14.6203 8.43209 14.8223 8.58558 14.9854L8.59 14.9896C8.75499 15.1449 8.96316 15.2155 9.20551 15.2062L9.20801 15.206ZM9.20847 19.3994C11.0634 19.2729 12.7631 19.0874 14.3075 18.8427C15.8589 18.5982 17.3934 18.3073 18.9112 17.97C19.2199 17.9014 19.4508 17.7891 19.566 17.6127C19.6783 17.4529 19.7347 17.2733 19.7347 17.0766C19.7347 16.8461 19.6469 16.6474 19.476 16.4861C19.2921 16.3123 19.0348 16.2718 18.7304 16.3345L18.729 16.3348C17.2827 16.6543 15.7942 16.9361 14.2635 17.18C12.7345 17.4236 11.045 17.6086 9.19495 17.7347C8.91804 17.7526 8.69771 17.8389 8.55608 18.0109C8.42276 18.1728 8.35645 18.3639 8.35645 18.5797C8.35645 18.8137 8.43209 19.0158 8.58558 19.1789L8.59 19.183C8.75499 19.3383 8.96316 19.4089 9.20551 19.3996L9.20847 19.3994ZM14.3075 23.007C12.7632 23.2518 11.0635 23.4331 9.20867 23.5512C8.9637 23.5698 8.75337 23.4982 8.58783 23.3326L8.58572 23.3305C8.433 23.1682 8.35645 22.9708 8.35645 22.7441C8.35645 22.521 8.42209 22.3253 8.55608 22.1626C8.69776 21.9906 8.91827 21.9043 9.19531 21.8864C11.0453 21.7687 12.7346 21.588 14.2635 21.3443C14.5555 21.2978 14.846 21.2499 15.135 21.2006C15.638 21.1148 16.1013 21.5003 16.1013 22.0121C16.1013 22.4073 15.8192 22.7472 15.4291 22.8163C15.0564 22.8823 14.6822 22.9459 14.3075 23.007Z"
fill="#000091"
fill="currentColor"
/>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -6,7 +6,9 @@ import {
useState,
} from 'react';
import { Button, Popover } from 'react-aria-components';
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import { useCunninghamTheme } from '@/cunningham';
import { BoxProps } from './Box';
@@ -27,11 +29,9 @@ const StyledButton = styled(Button)<StyledButtonProps>`
background: none;
outline: none;
transition: all 0.2s ease-in-out;
font-family: Marianne, Arial, serif;
font-weight: 500;
font-size: 0.938rem;
padding: 0;
text-wrap: nowrap;
${({ $css }) => $css};
`;
@@ -51,6 +51,8 @@ export const DropButton = ({
children,
label,
}: PropsWithChildren<DropButtonProps>) => {
const { themeTokens } = useCunninghamTheme();
const font = themeTokens['font']?.['families']['base'];
const [isLocalOpen, setIsLocalOpen] = useState(isOpen);
const triggerRef = useRef(null);
@@ -70,7 +72,10 @@ export const DropButton = ({
ref={triggerRef}
onPress={() => onOpenChangeHandler(true)}
aria-label={label}
$css={buttonCss}
$css={css`
font-family: ${font};
${buttonCss};
`}
className="--docs--drop-button"
>
{button}

View File

@@ -2,11 +2,17 @@ import { useQuery } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { Theme } from '@/cunningham/';
import { FooterType } from '@/features/footer';
import { PostHogConf } from '@/services';
interface ThemeCustomization {
footer?: FooterType;
}
interface ConfigResponse {
AI_FEATURE_ENABLED?: boolean;
COLLABORATION_WS_URL?: string;
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY?: boolean;
CRISP_WEBSITE_ID?: string;
ENVIRONMENT: string;
FRONTEND_CSS_URL?: string;
@@ -17,6 +23,7 @@ interface ConfigResponse {
MEDIA_BASE_URL?: string;
POSTHOG_KEY?: PostHogConf;
SENTRY_DSN?: string;
theme_customization?: ThemeCustomization;
}
const LOCAL_STORAGE_KEY = 'docs_config';

View File

@@ -2,8 +2,12 @@ import { useCunninghamTheme } from '../useCunninghamTheme';
describe('<useCunninghamTheme />', () => {
it('has the logo correctly set', () => {
const { themeTokens, setTheme } = useCunninghamTheme.getState();
setTheme('default');
expect(useCunninghamTheme.getState().themeTokens.logo?.src).toBe('');
// Change theme
useCunninghamTheme.getState().setTheme('dsfr');
const { themeTokens } = useCunninghamTheme.getState();
const logo = themeTokens.logo;
expect(logo?.src).toBe('/assets/logo-gouv.svg');
expect(logo?.widthHeader).toBe('110px');

View File

@@ -43,3 +43,36 @@
.c__tooltip {
padding: 4px 6px;
}
/**
* Image System
*/
.c__image-system-filter {
filter: var(--c--components--image-system-filter);
}
@font-face {
font-family: Inter;
font-style: italic;
font-weight: 100 900;
font-display: swap;
src: url('https://fonts.gstatic.com/s/inter/v18/UcCm3FwrK3iLTcvnUwQT9g.woff2')
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: Inter;
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('https://fonts.gstatic.com/s/inter/v18/UcCo3FwrK3iLTcviYwY.woff2')
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
U+2215, U+FEFF, U+FFFD;
}

View File

@@ -78,37 +78,42 @@
--c--theme--colors--greyscale-750: #353535;
--c--theme--colors--greyscale-950: #1e1e1e;
--c--theme--colors--greyscale-1000: #161616;
--c--theme--colors--danger-050: #fff4f4;
--c--theme--colors--blue-500: #417dc4;
--c--theme--colors--brown-500: #bd987a;
--c--theme--colors--cyan-500: #009099;
--c--theme--colors--gold-500: #c3992a;
--c--theme--colors--green-500: #00a95f;
--c--theme--colors--olive-500: #68a532;
--c--theme--colors--orange-500: #e4794a;
--c--theme--colors--purple-500: #a558a0;
--c--theme--colors--red-500: #e1000f;
--c--theme--colors--yellow-500: #b7a73f;
--c--theme--colors--rose-500: #e18b76;
--c--theme--colors--primary-action: #1212ff;
--c--theme--colors--primary-bg: #fafafa;
--c--theme--colors--primary-focus: #0a76f6;
--c--theme--colors--secondary-icon: var(--c--theme--colors--primary-text);
--c--theme--colors--blue-400: #7ab1e8;
--c--theme--colors--blue-500: #417dc4;
--c--theme--colors--blue-600: #3558a2;
--c--theme--colors--brown-400: #e6be92;
--c--theme--colors--brown-500: #bd987a;
--c--theme--colors--brown-600: #745b47;
--c--theme--colors--cyan-400: #34bab5;
--c--theme--colors--cyan-500: #009099;
--c--theme--colors--cyan-600: #006a6f;
--c--theme--colors--gold-400: #ffca00;
--c--theme--colors--gold-500: #c3992a;
--c--theme--colors--gold-600: #695240;
--c--theme--colors--green-400: #34cb6a;
--c--theme--colors--green-500: #00a95f;
--c--theme--colors--green-600: #297254;
--c--theme--colors--olive-400: #99c221;
--c--theme--colors--olive-500: #68a532;
--c--theme--colors--olive-600: #447049;
--c--theme--colors--orange-400: #ff732c;
--c--theme--colors--orange-500: #e4794a;
--c--theme--colors--orange-600: #755348;
--c--theme--colors--pink-400: #ffb7ae;
--c--theme--colors--pink-500: #e18b76;
--c--theme--colors--pink-600: #8d533e;
--c--theme--colors--purple-400: #ce70cc;
--c--theme--colors--purple-500: #a558a0;
--c--theme--colors--purple-600: #6e445a;
--c--theme--colors--yellow-400: #d8c634;
--c--theme--colors--yellow-500: #b7a73f;
--c--theme--colors--yellow-600: #66673d;
--c--theme--font--sizes--h1: 2rem;
--c--theme--font--sizes--h2: 1.75rem;
@@ -187,11 +192,15 @@
--c--theme--breakpoints--xxs: 320px;
--c--theme--breakpoints--mobile: 768px;
--c--theme--breakpoints--tablet: 1024px;
--c--theme--logo--src: /assets/logo-gouv.svg;
--c--theme--logo--widthheader: 110px;
--c--theme--logo--widthfooter: 220px;
--c--theme--logo--alt: gouvernement logo;
--c--theme--logo--src: ;
--c--theme--logo--alt: ;
--c--theme--logo--widthheader: ;
--c--theme--logo--widthfooter: ;
--c--components--modal--width-small: 342px;
--c--components--tooltip--padding: 4px 8px;
--c--components--tooltip--background-color: var(
--c--theme--colors--greyscale-1000
);
--c--components--button--medium-height: 40px;
--c--components--button--medium-text-height: 40px;
--c--components--button--border-radius: 4px;
@@ -448,8 +457,43 @@
--c--theme--colors--greyscale-950
);
--c--components--forms-select--font-size: 14px;
--c--components--la-gauffre--activated: true;
--c--components--home-proconnect--activated: true;
--c--components--badge--font-size: var(--c--theme--font--sizes--xs);
--c--components--badge--border-radius: 4px;
--c--components--badge--padding-inline: var(--c--theme--spacings--xs);
--c--components--badge--padding-block: var(--c--theme--spacings--2xs);
--c--components--badge--accent--background-color: var(
--c--theme--colors--primary-100
);
--c--components--badge--accent--color: var(--c--theme--colors--primary-600);
--c--components--badge--neutral--background-color: var(
--c--theme--colors--greyscale-100
);
--c--components--badge--neutral--color: var(
--c--theme--colors--greyscale-600
);
--c--components--badge--danger--background-color: var(
--c--theme--colors--danger-100
);
--c--components--badge--danger--color: var(--c--theme--colors--danger-600);
--c--components--badge--success--background-color: var(
--c--theme--colors--success-100
);
--c--components--badge--success--color: var(--c--theme--colors--success-600);
--c--components--badge--warning--background-color: var(
--c--theme--colors--warning-100
);
--c--components--badge--warning--color: var(--c--theme--colors--warning-600);
--c--components--badge--info--background-color: var(
--c--theme--colors--info-100
);
--c--components--badge--info--color: var(--c--theme--colors--info-600);
--c--components--la-gaufre: false;
--c--components--home-proconnect: false;
--c--components--beta: false;
--c--components--image-system-filter: ;
--c--components--favicon--ico: /assets/favicon-light.ico;
--c--components--favicon--png-light: /assets/favicon-light.png;
--c--components--favicon--png-dark: /assets/favicon-dark.png;
}
.cunningham-theme--dark {
@@ -501,6 +545,158 @@
--c--theme--colors--danger-900: #9d6666;
}
.cunningham-theme--dsfr {
--c--theme--colors--secondary-icon: #c9191e;
--c--theme--logo--src: /assets/logo-gouv.svg;
--c--theme--logo--widthHeader: 110px;
--c--theme--logo--widthFooter: 220px;
--c--theme--logo--alt: gouvernement logo;
--c--components--la-gaufre: true;
--c--components--home-proconnect: true;
--c--components--beta: true;
--c--components--favicon--ico: /assets/favicon-dsfr.ico;
--c--components--favicon--png-light: /assets/favicon-dsfr.png;
--c--components--favicon--png-dark: /assets/favicon-dark-dsfr.png;
}
.cunningham-theme--generic {
--c--theme--colors--primary-action: #206ebd;
--c--theme--colors--primary-focus: #1e64bf;
--c--theme--colors--primary-text: #2e2c28;
--c--theme--colors--primary-050: #f8f8f7;
--c--theme--colors--primary-100: #f0efec;
--c--theme--colors--primary-150: #f4f4fd;
--c--theme--colors--primary-200: #e8e7e4;
--c--theme--colors--primary-300: #cfcdc9;
--c--theme--colors--primary-400: #979592;
--c--theme--colors--primary-500: #82807d;
--c--theme--colors--primary-600: #3f3d39;
--c--theme--colors--primary-700: #2e2c28;
--c--theme--colors--primary-800: #302e29;
--c--theme--colors--primary-900: #282622;
--c--theme--colors--primary-950: #201f1c;
--c--theme--colors--secondary-text: #fff;
--c--theme--colors--secondary-50: #f4f7fa;
--c--theme--colors--secondary-100: #d7e3ee;
--c--theme--colors--secondary-200: #b8cce1;
--c--theme--colors--secondary-300: #99b4d3;
--c--theme--colors--secondary-400: #7595be;
--c--theme--colors--secondary-500: #5874a0;
--c--theme--colors--secondary-600: #3a5383;
--c--theme--colors--secondary-700: #1e3462;
--c--theme--colors--secondary-800: #091b41;
--c--theme--colors--secondary-900: #08183b;
--c--theme--colors--secondary-950: #071636;
--c--theme--colors--greyscale-text: #3c3b38;
--c--theme--colors--greyscale-000: #fff;
--c--theme--colors--greyscale-050: #f8f7f7;
--c--theme--colors--greyscale-100: #f3f3f2;
--c--theme--colors--greyscale-200: #ecebea;
--c--theme--colors--greyscale-250: #e4e3e2;
--c--theme--colors--greyscale-300: #d3d2cf;
--c--theme--colors--greyscale-350: #eee;
--c--theme--colors--greyscale-400: #96948e;
--c--theme--colors--greyscale-500: #817e77;
--c--theme--colors--greyscale-600: #6a6862;
--c--theme--colors--greyscale-700: #3c3b38;
--c--theme--colors--greyscale-750: #383632;
--c--theme--colors--greyscale-800: #2d2b27;
--c--theme--colors--greyscale-900: #262522;
--c--theme--colors--greyscale-950: #201f1c;
--c--theme--colors--greyscale-1000: #181714;
--c--theme--colors--success-text: #234935;
--c--theme--colors--success-50: #f3fbf5;
--c--theme--colors--success-100: #e4f7ea;
--c--theme--colors--success-200: #caeed4;
--c--theme--colors--success-300: #a0e0b5;
--c--theme--colors--success-400: #6cc88c;
--c--theme--colors--success-500: #6cc88c;
--c--theme--colors--success-600: #358d5c;
--c--theme--colors--success-700: #2d704b;
--c--theme--colors--success-800: #28583f;
--c--theme--colors--success-900: #234935;
--c--theme--colors--success-950: #0f281b;
--c--theme--colors--info-text: #212445;
--c--theme--colors--info-50: #f2f6fb;
--c--theme--colors--info-100: #e2e9f5;
--c--theme--colors--info-200: #ccd8ee;
--c--theme--colors--info-300: #a9c0e3;
--c--theme--colors--info-400: #809dd4;
--c--theme--colors--info-500: #617bc7;
--c--theme--colors--info-600: #4a5cbf;
--c--theme--colors--info-700: #3e49b2;
--c--theme--colors--info-800: #353c8f;
--c--theme--colors--info-900: #303771;
--c--theme--colors--info-950: #212445;
--c--theme--colors--warning-text: #d97c3a;
--c--theme--colors--warning-50: #fdf7f1;
--c--theme--colors--warning-100: #fbeddc;
--c--theme--colors--warning-200: #f5d9b9;
--c--theme--colors--warning-300: #edbe8c;
--c--theme--colors--warning-400: #e2985c;
--c--theme--colors--warning-500: #d97c3a;
--c--theme--colors--warning-600: #c96330;
--c--theme--colors--warning-700: #a34b32;
--c--theme--colors--warning-800: #813b2c;
--c--theme--colors--warning-900: #693327;
--c--theme--colors--warning-950: #381713;
--c--theme--colors--danger-action: #c0182a;
--c--theme--colors--danger-text: #fff;
--c--theme--colors--danger-050: #fdf5f4;
--c--theme--colors--danger-100: #fbebe8;
--c--theme--colors--danger-200: #f9e0dc;
--c--theme--colors--danger-300: #f3c3bd;
--c--theme--colors--danger-400: #e26552;
--c--theme--colors--danger-500: #c91f00;
--c--theme--colors--danger-600: #a71901;
--c--theme--colors--danger-700: #562c2b;
--c--theme--colors--danger-800: #392425;
--c--theme--colors--danger-900: #311f20;
--c--theme--colors--danger-950: #2a191a;
--c--theme--colors--blue-400: #8baecc;
--c--theme--colors--blue-500: #567aa2;
--c--theme--colors--blue-600: #455784;
--c--theme--colors--brown-400: #e4c090;
--c--theme--colors--brown-500: #ba9977;
--c--theme--colors--brown-600: #735c45;
--c--theme--colors--cyan-400: #5cbec9;
--c--theme--colors--cyan-500: #43a1b3;
--c--theme--colors--cyan-600: #39809b;
--c--theme--colors--gold-400: #ecbf50;
--c--theme--colors--gold-500: #dfa038;
--c--theme--colors--gold-600: #c17b31;
--c--theme--colors--green-400: #5dbd9a;
--c--theme--colors--green-500: #3aa183;
--c--theme--colors--green-600: #2a816d;
--c--theme--colors--olive-400: #afd662;
--c--theme--colors--olive-500: #90bb4b;
--c--theme--colors--olive-600: #6e9441;
--c--theme--colors--orange-400: #e2985c;
--c--theme--colors--orange-500: #d97c3a;
--c--theme--colors--orange-600: #c96330;
--c--theme--colors--pink-400: #be8fc8;
--c--theme--colors--pink-500: #a563b1;
--c--theme--colors--pink-600: #8b44a5;
--c--theme--colors--purple-400: #be8fc8;
--c--theme--colors--purple-500: #a563b1;
--c--theme--colors--purple-600: #8b44a5;
--c--theme--colors--yellow-400: #edc947;
--c--theme--colors--yellow-500: #dbb13a;
--c--theme--colors--yellow-600: #b88a34;
--c--theme--font--families--base: inter, roboto flex variable, sans-serif;
--c--theme--font--families--accent: inter, roboto flex variable, sans-serif;
--c--components--button--primary--background--color-hover: var(
--c--theme--colors--primary-focus
);
--c--components--button--primary--background--color-active: var(
--c--theme--colors--primary-focus
);
--c--components--button--primary--background--color-focus: var(
--c--theme--colors--primary-focus
);
--c--components--image-system-filter: saturate(0.2);
}
.clr-secondary-text {
color: var(--c--theme--colors--secondary-text);
}
@@ -817,6 +1013,54 @@
color: var(--c--theme--colors--greyscale-1000);
}
.clr-danger-050 {
color: var(--c--theme--colors--danger-050);
}
.clr-blue-500 {
color: var(--c--theme--colors--blue-500);
}
.clr-brown-500 {
color: var(--c--theme--colors--brown-500);
}
.clr-cyan-500 {
color: var(--c--theme--colors--cyan-500);
}
.clr-gold-500 {
color: var(--c--theme--colors--gold-500);
}
.clr-green-500 {
color: var(--c--theme--colors--green-500);
}
.clr-olive-500 {
color: var(--c--theme--colors--olive-500);
}
.clr-orange-500 {
color: var(--c--theme--colors--orange-500);
}
.clr-purple-500 {
color: var(--c--theme--colors--purple-500);
}
.clr-red-500 {
color: var(--c--theme--colors--red-500);
}
.clr-yellow-500 {
color: var(--c--theme--colors--yellow-500);
}
.clr-rose-500 {
color: var(--c--theme--colors--rose-500);
}
.clr-primary-action {
color: var(--c--theme--colors--primary-action);
}
@@ -825,12 +1069,16 @@
color: var(--c--theme--colors--primary-bg);
}
.clr-blue-400 {
color: var(--c--theme--colors--blue-400);
.clr-primary-focus {
color: var(--c--theme--colors--primary-focus);
}
.clr-blue-500 {
color: var(--c--theme--colors--blue-500);
.clr-secondary-icon {
color: var(--c--theme--colors--secondary-icon);
}
.clr-blue-400 {
color: var(--c--theme--colors--blue-400);
}
.clr-blue-600 {
@@ -841,10 +1089,6 @@
color: var(--c--theme--colors--brown-400);
}
.clr-brown-500 {
color: var(--c--theme--colors--brown-500);
}
.clr-brown-600 {
color: var(--c--theme--colors--brown-600);
}
@@ -853,10 +1097,6 @@
color: var(--c--theme--colors--cyan-400);
}
.clr-cyan-500 {
color: var(--c--theme--colors--cyan-500);
}
.clr-cyan-600 {
color: var(--c--theme--colors--cyan-600);
}
@@ -865,10 +1105,6 @@
color: var(--c--theme--colors--gold-400);
}
.clr-gold-500 {
color: var(--c--theme--colors--gold-500);
}
.clr-gold-600 {
color: var(--c--theme--colors--gold-600);
}
@@ -877,10 +1113,6 @@
color: var(--c--theme--colors--green-400);
}
.clr-green-500 {
color: var(--c--theme--colors--green-500);
}
.clr-green-600 {
color: var(--c--theme--colors--green-600);
}
@@ -889,10 +1121,6 @@
color: var(--c--theme--colors--olive-400);
}
.clr-olive-500 {
color: var(--c--theme--colors--olive-500);
}
.clr-olive-600 {
color: var(--c--theme--colors--olive-600);
}
@@ -901,10 +1129,6 @@
color: var(--c--theme--colors--orange-400);
}
.clr-orange-500 {
color: var(--c--theme--colors--orange-500);
}
.clr-orange-600 {
color: var(--c--theme--colors--orange-600);
}
@@ -925,10 +1149,6 @@
color: var(--c--theme--colors--purple-400);
}
.clr-purple-500 {
color: var(--c--theme--colors--purple-500);
}
.clr-purple-600 {
color: var(--c--theme--colors--purple-600);
}
@@ -937,10 +1157,6 @@
color: var(--c--theme--colors--yellow-400);
}
.clr-yellow-500 {
color: var(--c--theme--colors--yellow-500);
}
.clr-yellow-600 {
color: var(--c--theme--colors--yellow-600);
}
@@ -1261,6 +1477,54 @@
background-color: var(--c--theme--colors--greyscale-1000);
}
.bg-danger-050 {
background-color: var(--c--theme--colors--danger-050);
}
.bg-blue-500 {
background-color: var(--c--theme--colors--blue-500);
}
.bg-brown-500 {
background-color: var(--c--theme--colors--brown-500);
}
.bg-cyan-500 {
background-color: var(--c--theme--colors--cyan-500);
}
.bg-gold-500 {
background-color: var(--c--theme--colors--gold-500);
}
.bg-green-500 {
background-color: var(--c--theme--colors--green-500);
}
.bg-olive-500 {
background-color: var(--c--theme--colors--olive-500);
}
.bg-orange-500 {
background-color: var(--c--theme--colors--orange-500);
}
.bg-purple-500 {
background-color: var(--c--theme--colors--purple-500);
}
.bg-red-500 {
background-color: var(--c--theme--colors--red-500);
}
.bg-yellow-500 {
background-color: var(--c--theme--colors--yellow-500);
}
.bg-rose-500 {
background-color: var(--c--theme--colors--rose-500);
}
.bg-primary-action {
background-color: var(--c--theme--colors--primary-action);
}
@@ -1269,12 +1533,16 @@
background-color: var(--c--theme--colors--primary-bg);
}
.bg-blue-400 {
background-color: var(--c--theme--colors--blue-400);
.bg-primary-focus {
background-color: var(--c--theme--colors--primary-focus);
}
.bg-blue-500 {
background-color: var(--c--theme--colors--blue-500);
.bg-secondary-icon {
background-color: var(--c--theme--colors--secondary-icon);
}
.bg-blue-400 {
background-color: var(--c--theme--colors--blue-400);
}
.bg-blue-600 {
@@ -1285,10 +1553,6 @@
background-color: var(--c--theme--colors--brown-400);
}
.bg-brown-500 {
background-color: var(--c--theme--colors--brown-500);
}
.bg-brown-600 {
background-color: var(--c--theme--colors--brown-600);
}
@@ -1297,10 +1561,6 @@
background-color: var(--c--theme--colors--cyan-400);
}
.bg-cyan-500 {
background-color: var(--c--theme--colors--cyan-500);
}
.bg-cyan-600 {
background-color: var(--c--theme--colors--cyan-600);
}
@@ -1309,10 +1569,6 @@
background-color: var(--c--theme--colors--gold-400);
}
.bg-gold-500 {
background-color: var(--c--theme--colors--gold-500);
}
.bg-gold-600 {
background-color: var(--c--theme--colors--gold-600);
}
@@ -1321,10 +1577,6 @@
background-color: var(--c--theme--colors--green-400);
}
.bg-green-500 {
background-color: var(--c--theme--colors--green-500);
}
.bg-green-600 {
background-color: var(--c--theme--colors--green-600);
}
@@ -1333,10 +1585,6 @@
background-color: var(--c--theme--colors--olive-400);
}
.bg-olive-500 {
background-color: var(--c--theme--colors--olive-500);
}
.bg-olive-600 {
background-color: var(--c--theme--colors--olive-600);
}
@@ -1345,10 +1593,6 @@
background-color: var(--c--theme--colors--orange-400);
}
.bg-orange-500 {
background-color: var(--c--theme--colors--orange-500);
}
.bg-orange-600 {
background-color: var(--c--theme--colors--orange-600);
}
@@ -1369,10 +1613,6 @@
background-color: var(--c--theme--colors--purple-400);
}
.bg-purple-500 {
background-color: var(--c--theme--colors--purple-500);
}
.bg-purple-600 {
background-color: var(--c--theme--colors--purple-600);
}
@@ -1381,10 +1621,6 @@
background-color: var(--c--theme--colors--yellow-400);
}
.bg-yellow-500 {
background-color: var(--c--theme--colors--yellow-500);
}
.bg-yellow-600 {
background-color: var(--c--theme--colors--yellow-600);
}

View File

@@ -82,37 +82,42 @@ export const tokens = {
'greyscale-750': '#353535',
'greyscale-950': '#1E1E1E',
'greyscale-1000': '#161616',
'danger-050': '#FFF4F4',
'blue-500': '#417DC4',
'brown-500': '#BD987A',
'cyan-500': '#009099',
'gold-500': '#C3992A',
'green-500': '#00A95F',
'olive-500': '#68A532',
'orange-500': '#E4794A',
'purple-500': '#A558A0',
'red-500': '#E1000F',
'yellow-500': '#B7A73F',
'rose-500': '#E18B76',
'primary-action': '#1212FF',
'primary-bg': '#FAFAFA',
'primary-focus': '#0A76F6',
'secondary-icon': 'var(--c--theme--colors--primary-text)',
'blue-400': '#7AB1E8',
'blue-500': '#417DC4',
'blue-600': '#3558A2',
'brown-400': '#E6BE92',
'brown-500': '#BD987A',
'brown-600': '#745B47',
'cyan-400': '#34BAB5',
'cyan-500': '#009099',
'cyan-600': '#006A6F',
'gold-400': '#FFCA00',
'gold-500': '#C3992A',
'gold-600': '#695240',
'green-400': '#34CB6A',
'green-500': '#00A95F',
'green-600': '#297254',
'olive-400': '#99C221',
'olive-500': '#68A532',
'olive-600': '#447049',
'orange-400': '#FF732C',
'orange-500': '#E4794A',
'orange-600': '#755348',
'pink-400': '#FFB7AE',
'pink-500': '#E18B76',
'pink-600': '#8D533E',
'purple-400': '#CE70CC',
'purple-500': '#A558A0',
'purple-600': '#6E445A',
'yellow-400': '#D8C634',
'yellow-500': '#B7A73F',
'yellow-600': '#66673D',
},
font: {
@@ -205,79 +210,69 @@ export const tokens = {
mobile: '768px',
tablet: '1024px',
},
logo: {
src: '/assets/logo-gouv.svg',
widthHeader: '110px',
widthFooter: '220px',
alt: 'Gouvernement Logo',
},
logo: { src: '', alt: '', widthHeader: '', widthFooter: '' },
},
components: {
modal: { 'width-small': '342px' },
tooltip: { padding: '4px 8px', 'background-color': '#161616' },
button: {
'medium-height': '40px',
'medium-text-height': '40px',
'border-radius': '4px',
'small-height': '26px',
primary: {
'background--color': 'var(--c--theme--colors--primary-text)',
'background--color': '#000091',
'background--color-hover': '#1212ff',
'background--color-active': '#2323ff',
'background--color-disabled':
'var(--c--theme--colors--greyscale-100)',
'background--color-disabled': '#eee',
color: '#fff',
'color-hover': '#fff',
'color-active': '#fff',
'color-focus-visible': '#fff',
disabled: 'var(--c--theme--colors--greyscale-500)',
disabled: '#7C7C7C',
},
'primary-text': {
'background--color': 'var(--c--theme--colors--primary-text)',
'background--color-hover': 'var(--c--theme--colors--greyscale-100)',
'background--color-active': 'var(--c--theme--colors--primary-100)',
'background--color': '#000091',
'background--color-hover': '#eee',
'background--color-active': '#ECECFE',
'background--color-focus-visible': '#fff',
'background--color-disabled':
'var(--c--theme--colors--greyscale-000)',
color: 'var(--c--theme--colors--primary-800)',
'color-hover': 'var(--c--theme--colors--primary-800)',
disabled: 'var(--c--theme--colors--greyscale-400)',
'background--color-disabled': '#fff',
color: '#000091',
'color-hover': '#000091',
disabled: '#929292',
},
secondary: {
'background--color-hover': '#F6F6F6',
'background--color-active': '#EDEDED',
'background--color-focus-visible':
'var(--c--theme--colors--greyscale-000)',
'background--disabled': 'var(--c--theme--colors--greyscale-000)',
color: 'var(--c--theme--colors--primary-800)',
'border--color': 'var(--c--theme--colors--greyscale-300)',
'border--color-hover': 'var(--c--theme--colors--greyscale-300)',
'border--color-disabled': 'var(--c--theme--colors--greyscale-300)',
disabled: 'var(--c--theme--colors--greyscale-400)',
'background--color-focus-visible': '#fff',
'background--disabled': '#fff',
color: '#000091',
'border--color': '#CECECE',
'border--color-hover': '#CECECE',
'border--color-disabled': '#CECECE',
disabled: '#929292',
},
tertiary: {
'background--color': 'var(--c--theme--colors--primary-100)',
'background--color-focus-visible':
'var(--c--theme--colors--primary-100)',
'background--color-hover': 'var(--c--theme--colors--primary-300)',
'background--color-active': 'var(--c--theme--colors--primary-300)',
'background--disabled': 'var(--c--theme--colors--primary-050)',
color: 'var(--c--theme--colors--primary-800)',
disabled: 'var(--c--theme--colors--primary-300)',
'background--color': '#ECECFE',
'background--color-focus-visible': '#ECECFE',
'background--color-hover': '#CACAFB',
'background--color-active': '#CACAFB',
'background--disabled': '#F5F5FE',
color: '#000091',
disabled: '#CACAFB',
},
'tertiary-text': {
'background--color-hover': 'var(--c--theme--colors--greyscale-100)',
'color-hover': 'var(--c--theme--colors--primary-text)',
color: 'var(--c--theme--colors--primary-600)',
'background--color-hover': '#eee',
'color-hover': '#000091',
color: '#313178',
},
danger: {
'color-hover': 'white',
'background--color': 'var(--c--theme--colors--danger-600)',
'background--color': '#CE0500',
'background--color-hover': '#FF2725',
'background--color-focus-visible':
'var(--c--theme--colors--danger-600)',
'background--color-disabled':
'var(--c--theme--colors--greyscale-100)',
'color-disabled': 'var(--c--theme--colors--greyscale-400)',
'background--color-focus-visible': '#CE0500',
'background--color-disabled': '#eee',
'color-disabled': '#929292',
},
},
datagrid: {
@@ -288,22 +283,22 @@ export const tokens = {
},
'forms-checkbox': {
'border-radius': '4px',
'border-color': 'var(--c--theme--colors--primary-800)',
'background-color--hover': 'var(--c--theme--colors--greyscale-100)',
'border--color-disabled': 'var(--c--theme--colors--greyscale-200)',
'border--color': 'var(--c--theme--colors--primary-800)',
'background--disabled': 'var(--c--theme--colors--greyscale-200)',
'background--enable': 'var(--c--theme--colors--primary-800)',
'check--disabled': 'var(--c--theme--colors--greyscale-300)',
'check--enable': 'var(--c--theme--colors--greyscale-000)',
color: 'var(--c--theme--colors--primary-text)',
'label--color': 'var(--c--theme--colors--greyscale-1000)',
'label--size': 'var(--c--theme--font--sizes--sm)',
'border-color': '#000091',
'background-color--hover': '#eee',
'border--color-disabled': '#E5E5E5',
'border--color': '#000091',
'background--disabled': '#E5E5E5',
'background--enable': '#000091',
'check--disabled': '#CECECE',
'check--enable': '#fff',
color: '#000091',
'label--color': '#161616',
'label--size': '0.875rem',
'label--weight': '500',
'text--color': 'var(--c--theme--colors--greyscale-600)',
'text--size': 'var(--c--theme--font--sizes--s)',
'text--color': '#666666',
'text--size': '0.75rem',
'text--weight': '400',
'text--color-disabled': 'var(--c--theme--colors--greyscale-300)',
'text--color-disabled': '#CECECE',
},
'forms-labelledbox': {
'label-color--small': '#1E1E1E',
@@ -312,20 +307,18 @@ export const tokens = {
'label-color--big--disabled': '#CECECE',
},
'forms-radio': {
'border-color': 'var(--c--theme--colors--primary-800)',
'background-color': 'var(--c--theme--colors--greyscale-000)',
'accent-color': 'var(--c--theme--colors--primary-800)',
'accent-color-disabled': 'var(--c--theme--colors--greyscale-300)',
'border-color': '#000091',
'background-color': '#fff',
'accent-color': '#000091',
'accent-color-disabled': '#CECECE',
},
'forms-switch': {
'border--color-disabled': 'var(--c--theme--colors--greyscale-300)',
'border--color': 'var(--c--theme--colors--primary-800)',
'border--color-disabled': '#CECECE',
'border--color': '#000091',
'handle-background-color': 'white',
'handle-background-color--disabled':
'var(--c--theme--colors--greyscale-000)',
'rail-background-color--disabled':
'var(--c--theme--colors--greyscale-000)',
'accent-color': 'var(--c--theme--colors--primary-800)',
'handle-background-color--disabled': '#fff',
'rail-background-color--disabled': '#fff',
'accent-color': '#000091',
},
'forms-textarea': {
'label-color--focus': '#161616',
@@ -358,8 +351,27 @@ export const tokens = {
'value-color': '#1E1E1E',
'font-size': '14px',
},
'la-gauffre': { activated: true },
'home-proconnect': { activated: true },
badge: {
'font-size': '0.75rem',
'border-radius': '4px',
'padding-inline': '0.5rem',
'padding-block': '0.375rem',
accent: { 'background-color': '#ECECFE', color: '#313178' },
neutral: { 'background-color': '#eee', color: '#666666' },
danger: { 'background-color': '#FFE9E9', color: '#CE0500' },
success: { 'background-color': '#dffee6', color: '#18753c' },
warning: { 'background-color': '#fff4f3', color: '#b34000' },
info: { 'background-color': '#E8EDFF', color: '#0063CB' },
},
'la-gaufre': false,
'home-proconnect': false,
beta: false,
'image-system-filter': '',
favicon: {
ico: '/assets/favicon-light.ico',
'png-light': '/assets/favicon-light.png',
'png-dark': '/assets/favicon-dark.png',
},
},
},
dark: {
@@ -414,5 +426,173 @@ export const tokens = {
},
},
},
dsfr: {
theme: {
colors: { 'secondary-icon': '#C9191E' },
logo: {
src: '/assets/logo-gouv.svg',
widthHeader: '110px',
widthFooter: '220px',
alt: 'Gouvernement Logo',
},
},
components: {
'la-gaufre': true,
'home-proconnect': true,
beta: true,
favicon: {
ico: '/assets/favicon-dsfr.ico',
'png-light': '/assets/favicon-dsfr.png',
'png-dark': '/assets/favicon-dark-dsfr.png',
},
},
},
generic: {
theme: {
colors: {
'primary-action': '#206EBD',
'primary-focus': '#1E64BF',
'primary-text': '#2E2C28',
'primary-050': '#F8F8F7',
'primary-100': '#F0EFEC',
'primary-150': '#F4F4FD',
'primary-200': '#E8E7E4',
'primary-300': '#CFCDC9',
'primary-400': '#979592',
'primary-500': '#82807D',
'primary-600': '#3F3D39',
'primary-700': '#2E2C28',
'primary-800': '#302E29',
'primary-900': '#282622',
'primary-950': '#201F1C',
'secondary-text': '#fff',
'secondary-50': '#F4F7FA',
'secondary-100': '#D7E3EE',
'secondary-200': '#B8CCE1',
'secondary-300': '#99B4D3',
'secondary-400': '#7595BE',
'secondary-500': '#5874A0',
'secondary-600': '#3A5383',
'secondary-700': '#1E3462',
'secondary-800': '#091B41',
'secondary-900': '#08183B',
'secondary-950': '#071636',
'greyscale-text': '#3C3B38',
'greyscale-000': '#fff',
'greyscale-050': '#F8F7F7',
'greyscale-100': '#F3F3F2',
'greyscale-200': '#ECEBEA',
'greyscale-250': '#E4E3E2',
'greyscale-300': '#D3D2CF',
'greyscale-350': '#eee',
'greyscale-400': '#96948E',
'greyscale-500': '#817E77',
'greyscale-600': '#6A6862',
'greyscale-700': '#3C3B38',
'greyscale-750': '#383632',
'greyscale-800': '#2D2B27',
'greyscale-900': '#262522',
'greyscale-950': '#201F1C',
'greyscale-1000': '#181714',
'success-text': '#234935',
'success-50': '#F3FBF5',
'success-100': '#E4F7EA',
'success-200': '#CAEED4',
'success-300': '#A0E0B5',
'success-400': '#6CC88C',
'success-500': '#6CC88C',
'success-600': '#358D5C',
'success-700': '#2D704B',
'success-800': '#28583F',
'success-900': '#234935',
'success-950': '#0F281B',
'info-text': '#212445',
'info-50': '#F2F6FB',
'info-100': '#E2E9F5',
'info-200': '#CCD8EE',
'info-300': '#A9C0E3',
'info-400': '#809DD4',
'info-500': '#617BC7',
'info-600': '#4A5CBF',
'info-700': '#3E49B2',
'info-800': '#353C8F',
'info-900': '#303771',
'info-950': '#212445',
'warning-text': '#D97C3A',
'warning-50': '#FDF7F1',
'warning-100': '#FBEDDC',
'warning-200': '#F5D9B9',
'warning-300': '#EDBE8C',
'warning-400': '#E2985C',
'warning-500': '#D97C3A',
'warning-600': '#C96330',
'warning-700': '#A34B32',
'warning-800': '#813B2C',
'warning-900': '#693327',
'warning-950': '#381713',
'danger-action': '#C0182A',
'danger-text': '#FFF',
'danger-050': '#FDF5F4',
'danger-100': '#FBEBE8',
'danger-200': '#F9E0DC',
'danger-300': '#F3C3BD',
'danger-400': '#E26552',
'danger-500': '#C91F00',
'danger-600': '#A71901',
'danger-700': '#562C2B',
'danger-800': '#392425',
'danger-900': '#311F20',
'danger-950': '#2A191A',
'blue-400': '#8BAECC',
'blue-500': '#567AA2',
'blue-600': '#455784',
'brown-400': '#E4C090',
'brown-500': '#BA9977',
'brown-600': '#735C45',
'cyan-400': '#5CBEC9',
'cyan-500': '#43A1B3',
'cyan-600': '#39809B',
'gold-400': '#ECBF50',
'gold-500': '#DFA038',
'gold-600': '#C17B31',
'green-400': '#5DBD9A',
'green-500': '#3AA183',
'green-600': '#2A816D',
'olive-400': '#AFD662',
'olive-500': '#90BB4B',
'olive-600': '#6E9441',
'orange-400': '#E2985C',
'orange-500': '#D97C3A',
'orange-600': '#C96330',
'pink-400': '#BE8FC8',
'pink-500': '#A563B1',
'pink-600': '#8B44A5',
'purple-400': '#BE8FC8',
'purple-500': '#A563B1',
'purple-600': '#8B44A5',
'yellow-400': '#EDC947',
'yellow-500': '#DBB13A',
'yellow-600': '#B88A34',
},
font: {
families: {
base: 'Inter, Roboto Flex Variable, sans-serif',
accent: 'Inter, Roboto Flex Variable, sans-serif',
},
},
},
components: {
button: {
primary: {
background: {
'color-hover': 'var(--c--theme--colors--primary-focus)',
'color-active': 'var(--c--theme--colors--primary-focus)',
'color-focus': 'var(--c--theme--colors--primary-focus)',
},
},
},
'image-system-filter': 'saturate(0.2)',
},
},
},
};

View File

@@ -12,30 +12,32 @@ type ComponentTokens = Tokens['components'];
export type Theme = keyof typeof tokens.themes;
interface ThemeStore {
theme: Theme;
setTheme: (theme: Theme) => void;
themeTokens: Partial<Tokens['theme']>;
colorsTokens: Partial<ColorsTokens>;
fontSizesTokens: Partial<FontSizesTokens>;
spacingsTokens: Partial<SpacingsTokens>;
componentTokens: ComponentTokens;
currentTokens: Partial<Tokens>;
fontSizesTokens: Partial<FontSizesTokens>;
setTheme: (theme: Theme) => void;
spacingsTokens: Partial<SpacingsTokens>;
theme: Theme;
themeTokens: Partial<Tokens['theme']>;
}
const getMergedTokens = (theme: Theme) => {
return merge({}, tokens.themes['default'], tokens.themes[theme]);
};
const DEFAULT_THEME: Theme = 'default';
const DEFAULT_THEME: Theme = 'generic';
const defaultTokens = getMergedTokens(DEFAULT_THEME);
const initialState: ThemeStore = {
theme: DEFAULT_THEME,
setTheme: () => {},
themeTokens: defaultTokens.theme,
colorsTokens: defaultTokens.theme.colors,
componentTokens: defaultTokens.components,
spacingsTokens: defaultTokens.theme.spacings,
currentTokens: tokens.themes[DEFAULT_THEME] as Partial<Tokens>,
fontSizesTokens: defaultTokens.theme.font.sizes,
setTheme: () => {},
spacingsTokens: defaultTokens.theme.spacings,
theme: DEFAULT_THEME,
themeTokens: defaultTokens.theme,
};
export const useCunninghamTheme = create<ThemeStore>((set) => ({
@@ -44,12 +46,13 @@ export const useCunninghamTheme = create<ThemeStore>((set) => ({
const newTokens = getMergedTokens(theme);
set({
theme,
themeTokens: newTokens.theme,
colorsTokens: newTokens.theme.colors,
componentTokens: newTokens.components,
spacingsTokens: newTokens.theme.spacings,
currentTokens: tokens.themes[theme] as Partial<Tokens>,
fontSizesTokens: newTokens.theme.font.sizes,
spacingsTokens: newTokens.theme.spacings,
theme,
themeTokens: newTokens.theme,
});
},
}));

View File

@@ -13,7 +13,13 @@ declare module '*.svg' {
}
declare module '*.svg?url' {
const content: string;
const content: {
src: string;
width: number;
height: number;
blurWidth: number;
blurHeight: number;
};
export default content;
}

View File

@@ -5,6 +5,7 @@ import { PropsWithChildren } from 'react';
import { Box } from '@/components';
import { useConfig } from '@/core';
import { HOME_URL } from '../conf';
import { useAuth } from '../hooks';
import { getAuthUrl, gotoLogin } from '../utils';
@@ -43,7 +44,7 @@ export const Auth = ({ children }: PropsWithChildren) => {
*/
if (!authenticated && !pathAllowed) {
if (config?.FRONTEND_HOMEPAGE_FEATURE_ENABLED) {
void replace('/home');
void replace(HOME_URL);
} else {
gotoLogin();
}
@@ -57,7 +58,7 @@ export const Auth = ({ children }: PropsWithChildren) => {
/**
* If the user is authenticated and the path is the home page, we redirect to the index.
*/
if (pathname === '/home' && authenticated) {
if (pathname === HOME_URL && authenticated) {
void replace('/');
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">

View File

@@ -1,5 +1,6 @@
import { baseApiUrl } from '@/api';
export const PATH_AUTH_LOCAL_STORAGE = 'docs-path-auth';
export const HOME_URL = '/home';
export const LOGIN_URL = `${baseApiUrl()}authenticate/`;
export const LOGOUT_URL = `${baseApiUrl()}logout/`;
export const PATH_AUTH_LOCAL_STORAGE = 'docs-path-auth';

View File

@@ -1,4 +1,5 @@
export * from './api';
export * from './components';
export * from './conf';
export * from './hooks';
export * from './utils';

View File

@@ -0,0 +1,27 @@
import { APIError, errorCauses } from '@/api';
interface CheckDocMediaStatusResponse {
file?: string;
status: 'processing' | 'ready';
}
interface CheckDocMediaStatus {
urlMedia: string;
}
export const checkDocMediaStatus = async ({
urlMedia,
}: CheckDocMediaStatus): Promise<CheckDocMediaStatusResponse> => {
const response = await fetch(urlMedia, {
credentials: 'include',
});
if (!response.ok) {
throw new APIError(
'Failed to check the media status',
await errorCauses(response),
);
}
return response.json() as Promise<CheckDocMediaStatusResponse>;
};

View File

@@ -1,3 +1,4 @@
export * from './checkDocMediaStatus';
export * from './useCreateDocUpload';
export * from './useDocAITransform';
export * from './useDocAITranslate';

View File

@@ -0,0 +1,27 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#3b82f6"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
class="analyzing-spinner"
>
<circle cx="12" cy="12" r="10" opacity="0.2"></circle>
<path d="M12 2a10 10 0 0 1 10 10" opacity="0.8">
<animateTransform
attributeName="transform"
type="rotate"
from="0 12 12"
to="360 12 12"
dur="1s"
repeatCount="indefinite"
calcMode="spline"
keySplines="0.42 0 0.58 1"
keyTimes="0;1"
/>
</path>
</svg>

After

Width:  |  Height:  |  Size: 598 B

View File

@@ -0,0 +1,17 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
fill="#FFA500"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>

After

Width:  |  Height:  |  Size: 425 B

View File

@@ -15,14 +15,14 @@ import { useTranslation } from 'react-i18next';
import * as Y from 'yjs';
import { Box, TextErrors } from '@/components';
import { Doc } from '@/docs/doc-management';
import { Doc, useIsCollaborativeEditable } from '@/docs/doc-management';
import { useAuth } from '@/features/auth';
import { useUploadFile } from '../hook';
import { useHeadings } from '../hook/useHeadings';
import { useHeadings, useUploadFile, useUploadStatus } from '../hook/';
import useSaveDoc from '../hook/useSaveDoc';
import { useEditorStore } from '../stores';
import { cssEditor } from '../styles';
import { DocsBlockNoteEditor } from '../types';
import { randomColor } from '../utils';
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
@@ -49,7 +49,9 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
const { setEditor } = useEditorStore();
const { t } = useTranslation();
const readOnly = !doc.abilities.partial_update;
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
useSaveDoc(doc.id, provider.document, !readOnly);
const { i18n } = useTranslation();
const lang = i18n.resolvedLanguage;
@@ -61,7 +63,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
: user?.full_name || user?.email || t('Anonymous');
const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity';
const editor = useCreateBlockNote(
const editor: DocsBlockNoteEditor = useCreateBlockNote(
{
codeBlock,
collaboration: {
@@ -125,7 +127,9 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
},
[collabName, lang, provider, uploadFile],
);
useHeadings(editor);
useUploadStatus(editor);
useEffect(() => {
setEditor(editor);

View File

@@ -32,7 +32,6 @@ export const ModalConfirmDownloadUnsafe = ({
aria-label={t('Download')}
color="danger"
onClick={() => {
console.log('onClick');
if (onConfirm) {
void onConfirm();
}

View File

@@ -25,7 +25,6 @@ interface DocEditorProps {
export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
const { isDesktop } = useResponsiveStore();
const isVersion = !!versionId && typeof versionId === 'string';
const { colorsTokens } = useCunninghamTheme();

View File

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

View File

@@ -1,11 +1,85 @@
import { useCallback } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { backendUrl } from '@/api';
import { useMediaUrl } from '@/core/config';
import { sleep } from '@/utils';
import { useCreateDocAttachment } from '../api';
import { checkDocMediaStatus, useCreateDocAttachment } from '../api';
import Loader from '../assets/loader.svg?url';
import Warning from '../assets/warning.svg?url';
import { DocsBlockNoteEditor } from '../types';
/**
* Upload file can be analyzed on the server side,
* we had this function to wait for the analysis to be done
* before returning the file url. It will keep the loader
* on the upload button until the analysis is done.
* @param url
* @returns Promise<CheckDocMediaStatusResponse> status_code
* @description Waits for the upload to be analyzed by checking the status of the file.
*/
const loopCheckDocMediaStatus = async (url: string) => {
const SLEEP_TIME = 5000;
const response = await checkDocMediaStatus({
urlMedia: url,
});
if (response.status === 'ready') {
return response;
} else {
await sleep(SLEEP_TIME);
return await loopCheckDocMediaStatus(url);
}
};
const informationStatus = (src: string, text: string) => {
const loadingContainer = document.createElement('div');
loadingContainer.style.display = 'flex';
loadingContainer.style.alignItems = 'center';
loadingContainer.style.justifyContent = 'left';
loadingContainer.style.padding = '10px';
loadingContainer.style.color = '#666';
loadingContainer.className =
'bn-visual-media bn-audio bn-file-name-with-icon';
// Create an image element for the SVG
const imgElement = document.createElement('img');
imgElement.src = src;
// Create a text span
const textSpan = document.createElement('span');
textSpan.textContent = text;
textSpan.style.marginLeft = '8px';
textSpan.style.verticalAlign = 'middle';
imgElement.style.animation = 'spin 1.5s linear infinite';
// Add the spinner and text to the container
loadingContainer.appendChild(imgElement);
loadingContainer.appendChild(textSpan);
return loadingContainer;
};
const replaceUploadContent = (blockId: string, elementReplace: HTMLElement) => {
const blockEl = document.body.querySelector(
`.bn-block[data-id="${blockId}"]`,
);
blockEl
?.querySelector('.bn-visual-media-wrapper .bn-visual-media')
?.replaceWith(elementReplace);
blockEl
?.querySelector('.bn-file-block-content-wrapper .bn-audio')
?.replaceWith(elementReplace);
blockEl
?.querySelector('.bn-file-block-content-wrapper .bn-file-name-with-icon')
?.replaceWith(elementReplace);
};
export const useUploadFile = (docId: string) => {
const mediaUrl = useMediaUrl();
const {
mutateAsync: createDocAttachment,
isError: isErrorAttachment,
@@ -22,9 +96,9 @@ export const useUploadFile = (docId: string) => {
body,
});
return `${mediaUrl}${ret.file}`;
return `${backendUrl()}${ret.file}`;
},
[createDocAttachment, docId, mediaUrl],
[createDocAttachment, docId],
);
return {
@@ -33,3 +107,101 @@ export const useUploadFile = (docId: string) => {
errorAttachment,
};
};
export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
const ANALYZE_URL = 'media-check';
const { t } = useTranslation();
const mediaUrl = useMediaUrl();
const timeoutIds = useRef<Record<string, NodeJS.Timeout>>({});
const blockAnalyzeProcess = useCallback(
(editor: DocsBlockNoteEditor, blockId: string, url: string) => {
if (timeoutIds.current[url]) {
clearTimeout(timeoutIds.current[url]);
}
// Delay to let the time to the dom to be rendered
const timoutId = setTimeout(() => {
replaceUploadContent(
blockId,
informationStatus(Loader.src, t('Analyzing file...')),
);
loopCheckDocMediaStatus(url)
.then((response) => {
const block = editor.getBlock(blockId);
if (!block) {
return;
}
block.props = {
...block.props,
url: `${mediaUrl}${response.file}`,
};
editor.updateBlock(blockId, block);
})
.catch((error) => {
console.error('Error analyzing file:', error);
replaceUploadContent(
blockId,
informationStatus(
Warning.src,
t('The antivirus has detected an anomaly in your file.'),
),
);
});
}, 250);
timeoutIds.current[url] = timoutId;
},
[t, mediaUrl],
);
useEffect(() => {
const blocksAnalyze = editor?.document.filter(
(block) => 'url' in block.props && block.props.url.includes(ANALYZE_URL),
);
if (!blocksAnalyze?.length) {
return;
}
blocksAnalyze.forEach((block) => {
if (!('url' in block.props)) {
return;
}
blockAnalyzeProcess(editor, block.id, block.props.url);
});
}, [blockAnalyzeProcess, editor]);
useEffect(() => {
editor.onChange((_, context) => {
const blocksChanges = context.getChanges();
if (!blocksChanges.length) {
return;
}
const blockChanges = blocksChanges[0];
if (
blockChanges.source.type !== 'local' ||
blockChanges.type !== 'update' ||
!('url' in blockChanges.block.props) ||
('url' in blockChanges.block.props &&
!blockChanges.block.props.url.includes(ANALYZE_URL))
) {
return;
}
blockAnalyzeProcess(
editor,
blockChanges.block.id,
blockChanges.block.props.url,
);
});
}, [blockAnalyzeProcess, mediaUrl, editor, t]);
};

View File

@@ -21,6 +21,7 @@ export const blockMappingImageDocx: DocsExporterDocx['mappings']['blockMapping']
const blob = await exporter.resolveFile(block.props.url);
let pngConverted: string | undefined;
let dimensions: { width: number; height: number } | undefined;
let previewWidth = block.props.previewWidth || undefined;
if (!blob.type.includes('image')) {
return [];
@@ -28,7 +29,9 @@ export const blockMappingImageDocx: DocsExporterDocx['mappings']['blockMapping']
if (blob.type.includes('svg')) {
const svgText = await blob.text();
pngConverted = await convertSvgToPng(svgText, block.props.previewWidth);
const FALLBACK_SIZE = 536;
previewWidth = previewWidth || blob.size || FALLBACK_SIZE;
pngConverted = await convertSvgToPng(svgText, previewWidth);
const img = new Image();
img.src = pngConverted;
await new Promise((resolve) => {
@@ -47,8 +50,7 @@ export const blockMappingImageDocx: DocsExporterDocx['mappings']['blockMapping']
const { width, height } = dimensions;
let previewWidth = block.props.previewWidth;
if (previewWidth > MAX_WIDTH) {
if (previewWidth && previewWidth > MAX_WIDTH) {
previewWidth = MAX_WIDTH;
}
@@ -69,8 +71,8 @@ export const blockMappingImageDocx: DocsExporterDocx['mappings']['blockMapping']
}
: undefined,
transformation: {
width: previewWidth,
height: (previewWidth / width) * height,
width: previewWidth || width,
height: ((previewWidth || width) / width) * height,
},
}),
],

View File

@@ -12,6 +12,7 @@ export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['
async (block, exporter) => {
const blob = await exporter.resolveFile(block.props.url);
let pngConverted: string | undefined;
let width = block.props.previewWidth || undefined;
if (!blob.type.includes('image')) {
return <View wrap={false} />;
@@ -19,7 +20,9 @@ export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['
if (blob.type.includes('svg')) {
const svgText = await blob.text();
pngConverted = await convertSvgToPng(svgText, block.props.previewWidth);
const FALLBACK_SIZE = 536;
width = width || blob.size || FALLBACK_SIZE;
pngConverted = await convertSvgToPng(svgText, width);
}
return (
@@ -27,7 +30,7 @@ export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['
<Image
src={pngConverted || blob}
style={{
width: block.props.previewWidth * PIXELS_PER_POINT,
width: width ? width * PIXELS_PER_POINT : undefined,
maxWidth: '100%',
}}
/>

View File

@@ -0,0 +1,113 @@
import { Button, Modal, ModalSize } from '@openfun/cunningham-react';
import { t } from 'i18next';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, BoxButton, Icon, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
export const AlertNetwork = () => {
const { t } = useTranslation();
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<>
<Box>
<Box
$direction="row"
$justify="space-between"
$width="100%"
$background={colorsTokens['warning-100']}
$radius={spacingsTokens['3xs']}
$padding="xs"
$flex={1}
$align="center"
$gap={spacingsTokens['3xs']}
$css={css`
border: 1px solid var(--c--theme--colors--warning-300);
`}
>
<Box $direction="row" $gap={spacingsTokens['2xs']} $align="center">
<Icon iconName="mobiledata_off" $theme="warning" $variation="600" />
<Text $theme="warning" $variation="600" $weight={500}>
{t('Your network do not allow you to edit')}
</Text>
</Box>
<BoxButton
$direction="row"
$gap={spacingsTokens['3xs']}
$align="center"
onClick={() => setIsModalOpen(true)}
>
<Icon
iconName="info"
$theme="warning"
$variation="600"
$size="16px"
$weight="500"
$margin={{ top: 'auto' }}
/>
<Text $theme="warning" $variation="600" $weight="500" $size="xs">
{t('Know more')}
</Text>
</BoxButton>
</Box>
</Box>
{isModalOpen && (
<AlertNetworkModal onClose={() => setIsModalOpen(false)} />
)}
</>
);
};
interface AlertNetworkModalProps {
onClose: () => void;
}
export const AlertNetworkModal = ({ onClose }: AlertNetworkModalProps) => {
return (
<Modal
isOpen
closeOnClickOutside
onClose={() => onClose()}
rightActions={
<>
<Button aria-label={t('OK')} onClick={onClose}>
{t('OK')}
</Button>
</>
}
size={ModalSize.MEDIUM}
title={
<Text
$size="h6"
as="h6"
$margin={{ all: '0' }}
$align="flex-start"
$variation="1000"
>
{t("Why can't I edit?")}
</Text>
}
>
<Box
aria-label={t('Content modal to explain why the user cannot edit')}
className="--docs--modal-alert-network"
$margin={{ top: 'xs' }}
>
<Text $size="sm" $variation="600">
{t(
'The network configuration of your workstation or internet connection does not allow editing shared documents.',
)}
</Text>
<Text $size="sm" $variation="600" $margin={{ top: 'xs' }}>
{t(
'Docs use WebSockets to enable real-time editing. These communication channels allow instant and bidirectional exchanges between your browser and our servers. To access collaborative editing, please contact your IT department to enable WebSockets.',
)}
</Text>
</Box>
</Modal>
);
};

View File

@@ -0,0 +1,39 @@
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, Icon, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
export const AlertPublic = ({ isPublicDoc }: { isPublicDoc: boolean }) => {
const { t } = useTranslation();
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
return (
<Box
aria-label={t('Public document')}
$color={colorsTokens['primary-800']}
$background={colorsTokens['primary-050']}
$radius={spacingsTokens['3xs']}
$direction="row"
$padding="xs"
$flex={1}
$align="center"
$gap={spacingsTokens['3xs']}
$css={css`
border: 1px solid var(--c--theme--colors--primary-300, #e3e3fd);
`}
>
<Icon
$theme="primary"
$variation="800"
data-testid="public-icon"
iconName={isPublicDoc ? 'public' : 'vpn_lock'}
/>
<Text $theme="primary" $variation="800" $weight="500">
{isPublicDoc
? t('Public document')
: t('Document accessible to any connected person')}
</Text>
</Box>
);
};

View File

@@ -1,17 +1,20 @@
import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, HorizontalSeparator, Icon, Text } from '@/components';
import { Box, HorizontalSeparator, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import {
Doc,
LinkReach,
Role,
currentDocRole,
useIsCollaborativeEditable,
useTrans,
} from '@/docs/doc-management';
import { useResponsiveStore } from '@/stores';
import { AlertNetwork } from './AlertNetwork';
import { AlertPublic } from './AlertPublic';
import { DocTitle } from './DocTitle';
import { DocToolBox } from './DocToolBox';
@@ -20,51 +23,26 @@ interface DocHeaderProps {
}
export const DocHeader = ({ doc }: DocHeaderProps) => {
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
const { spacingsTokens } = useCunninghamTheme();
const { isDesktop } = useResponsiveStore();
const { t } = useTranslation();
const { transRole } = useTrans();
const { isEditable } = useIsCollaborativeEditable(doc);
const docIsPublic = doc.link_reach === LinkReach.PUBLIC;
const docIsAuth = doc.link_reach === LinkReach.AUTHENTICATED;
const { transRole } = useTrans();
return (
<>
<Box
$width="100%"
$padding={{ top: isDesktop ? '4xl' : 'md' }}
$padding={{ top: isDesktop ? '50px' : 'md' }}
$gap={spacingsTokens['base']}
aria-label={t('It is the card information about the document.')}
className="--docs--doc-header"
>
{!isEditable && <AlertNetwork />}
{(docIsPublic || docIsAuth) && (
<Box
aria-label={t('Public document')}
$color={colorsTokens['primary-800']}
$background={colorsTokens['primary-050']}
$radius={spacingsTokens['3xs']}
$direction="row"
$padding="xs"
$flex={1}
$align="center"
$gap={spacingsTokens['3xs']}
$css={css`
border: 1px solid var(--c--theme--colors--primary-300, #e3e3fd);
`}
>
<Icon
$theme="primary"
$variation="800"
data-testid="public-icon"
iconName={docIsPublic ? 'public' : 'vpn_lock'}
/>
<Text $theme="primary" $variation="800">
{docIsPublic
? t('Public document')
: t('Document accessible to any connected person')}
</Text>
</Box>
<AlertPublic isPublicDoc={docIsPublic} />
)}
<Box
$direction="row"
@@ -86,8 +64,18 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
<Box $direction="row">
{isDesktop && (
<>
<Text $variation="600" $size="s" $weight="bold">
{transRole(currentDocRole(doc.abilities))}&nbsp;·&nbsp;
<Text
$variation="600"
$size="s"
$weight="bold"
$theme={isEditable ? 'greyscale' : 'warning'}
>
{transRole(
isEditable
? currentDocRole(doc.abilities)
: Role.READER,
)}
&nbsp;·&nbsp;
</Text>
<Text $variation="600" $size="s">
{t('Last update: {{update}}', {

View File

@@ -1,3 +1,4 @@
export * from './useCollaboration';
export * from './useTrans';
export * from './useCopyDocLink';
export * from './useIsCollaborativeEditable';
export * from './useTrans';

View File

@@ -0,0 +1,53 @@
import { useEffect, useState } from 'react';
import { useConfig } from '@/core';
import { useIsOffline } from '@/features/service-worker';
import { useProviderStore } from '../stores';
import { Doc, LinkReach } from '../types';
export const useIsCollaborativeEditable = (doc: Doc) => {
const { isConnected } = useProviderStore();
const { data: conf } = useConfig();
const docIsPublic = doc.link_reach === LinkReach.PUBLIC;
const docIsAuth = doc.link_reach === LinkReach.AUTHENTICATED;
const docHasMember = doc.nb_accesses_direct > 1;
const isShared = docIsPublic || docIsAuth || docHasMember;
const [isEditable, setIsEditable] = useState(true);
const [isLoading, setIsLoading] = useState(true);
const { isOffline } = useIsOffline();
/**
* Connection can take a few seconds
*/
useEffect(() => {
const _isEditable = isConnected || !isShared || isOffline;
setIsLoading(true);
if (_isEditable) {
setIsEditable(true);
setIsLoading(false);
return;
}
const timer = setTimeout(() => {
setIsEditable(false);
setIsLoading(false);
}, 5000);
return () => clearTimeout(timer);
}, [isConnected, isOffline, isShared]);
if (!conf?.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY) {
return {
isEditable: true,
isLoading: false,
};
}
return {
isEditable,
isLoading,
};
};

View File

@@ -1,4 +1,4 @@
import { HocuspocusProvider } from '@hocuspocus/provider';
import { HocuspocusProvider, WebSocketStatus } from '@hocuspocus/provider';
import * as Y from 'yjs';
import { create } from 'zustand';
@@ -12,10 +12,12 @@ export interface UseCollaborationStore {
) => HocuspocusProvider;
destroyProvider: () => void;
provider: HocuspocusProvider | undefined;
isConnected: boolean;
}
const defaultValues = {
provider: undefined,
isConnected: false,
};
export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
@@ -33,6 +35,11 @@ export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
url: wsUrl,
name: storeId,
document: doc,
onStatus: ({ status }) => {
set({
isConnected: status === WebSocketStatus.Connected,
});
},
});
set({

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