mirror of
https://github.com/suitenumerique/docs.git
synced 2026-05-07 07:32:33 +02:00
Compare commits
31 Commits
v3.2.0
...
fix/docker
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9aa0cb7788 | ||
|
|
dd6e0b5072 | ||
|
|
95d3a8cd18 | ||
|
|
4f126ab824 | ||
|
|
fb90c13dad | ||
|
|
4118d79525 | ||
|
|
5848f43cb4 | ||
|
|
4b0fd223c8 | ||
|
|
31d0733851 | ||
|
|
16e20e984c | ||
|
|
76c28760dc | ||
|
|
d856abb5d8 | ||
|
|
25abd964de | ||
|
|
a070e1dd87 | ||
|
|
37d9ae8cca | ||
|
|
29ea6b8ef7 | ||
|
|
a692fa6f39 | ||
|
|
4d541c5d52 | ||
|
|
e5f029ad1d | ||
|
|
bd79f84e07 | ||
|
|
a070f56339 | ||
|
|
02478acb3f | ||
|
|
23aa497db0 | ||
|
|
d48436bffb | ||
|
|
41e4c45934 | ||
|
|
6be87ed477 | ||
|
|
c96182b3e3 | ||
|
|
e79d1d618a | ||
|
|
2691cdd4a2 | ||
|
|
05a1390bdc | ||
|
|
dfe8ae14fe |
4
.github/workflows/docker-hub.yml
vendored
4
.github/workflows/docker-hub.yml
vendored
@@ -79,7 +79,9 @@ jobs:
|
||||
context: .
|
||||
file: ./src/frontend/Dockerfile
|
||||
target: frontend-production
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
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 }}
|
||||
|
||||
19
.github/workflows/impress.yml
vendored
19
.github/workflows/impress.yml
vendored
@@ -61,6 +61,25 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
lint-spell-mistakes:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Install codespell
|
||||
run: pip install --user codespell
|
||||
- name: Check for typos
|
||||
run: |
|
||||
codespell \
|
||||
--check-filenames \
|
||||
--ignore-words-list "Dokument,afterAll,excpt,statics" \
|
||||
--skip "./git/" \
|
||||
--skip "**/*.po" \
|
||||
--skip "**/*.pot" \
|
||||
--skip "**/*.json" \
|
||||
--skip "**/yarn.lock"
|
||||
|
||||
lint-back:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
|
||||
31
CHANGELOG.md
31
CHANGELOG.md
@@ -8,12 +8,38 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## Added
|
||||
|
||||
- ✨(back) allow theme customnization using a configuration file #948
|
||||
- ✨ Add a custom callout block to the editor #892
|
||||
- 🚩(frontend) version MIT only #911
|
||||
- ✨(backend) integrate maleware_detection from django-lasuite #936
|
||||
- 🩺(CI) add lint spell mistakes #954
|
||||
|
||||
## Changed
|
||||
|
||||
- 📝(frontend) Update documentation
|
||||
- ✅(frontend) Improve tests coverage
|
||||
|
||||
### Removed
|
||||
|
||||
- 🔥(back) remove footer endpoint
|
||||
|
||||
## [3.2.1] - 2025-05-06
|
||||
|
||||
## Fixed
|
||||
|
||||
- 🐛(frontend) fix list copy paste #943
|
||||
- 📝(doc) update contributing policy (commit signatures are now mandatory) #895
|
||||
|
||||
|
||||
## [3.2.0] - 2025-05-05
|
||||
|
||||
## Added
|
||||
|
||||
- 🚸(backend) make document search on title accent-insensitive #874
|
||||
- 🚩 add homepage feature flag #861
|
||||
- 📝(doc) update contributing policy (commit signatures are now mandatory) #895
|
||||
- ✨(settings) Allow configuring PKCE for the SSO #886
|
||||
- 🌐(i18n) activate chinese and spanish languages #884
|
||||
- 🔧(backend) allow overwriting the data directory #893
|
||||
@@ -531,7 +557,7 @@ and this project adheres to
|
||||
- ⚡️(e2e) unique login between tests (#80)
|
||||
- ⚡️(CI) improve e2e job (#86)
|
||||
- ♻️(frontend) improve the error and message info ui (#93)
|
||||
- ✏️(frontend) change all occurences of pad to doc (#99)
|
||||
- ✏️(frontend) change all occurrences of pad to doc (#99)
|
||||
|
||||
## Fixed
|
||||
|
||||
@@ -549,7 +575,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.0...main
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v3.2.1...main
|
||||
[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
|
||||
[v3.0.0]: https://github.com/numerique-gouv/impress/releases/v3.0.0
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
Thank you for taking the time to contribute! Please follow these guidelines to ensure a smooth and productive workflow. 🚀🚀🚀
|
||||
|
||||
To get started with the project, please refer to the [README.md](https://github.com/suitenumerique/docs/blob/main/README.md) for detailed instructions.
|
||||
To get started with the project, please refer to the [README.md](https://github.com/suitenumerique/docs/blob/main/README.md) for detailed instructions on how to run Docs locally.
|
||||
|
||||
Contributors are required to sign off their commits with `git commit --sign-off`: this confirms that they have read and accepted the [Developer's Certificate of Origin 1.1](https://developercertificate.org/).
|
||||
Contributors are required to sign off their commits with `git commit --signoff`: this confirms that they have read and accepted the [Developer's Certificate of Origin 1.1](https://developercertificate.org/). For security reasons we also require [signing your commits with your SSH or GPG key](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) with `git commit -S`.
|
||||
|
||||
Please also check out our [dev handbook](https://suitenumerique.gitbook.io/handbook) to learn our best practices.
|
||||
|
||||
## Help us with translations
|
||||
|
||||
You can help us with translations on [Crowdin](https://crowdin.com/project/lasuite-docs).
|
||||
Your language is not there? Request it on our Crowdin page 😊.
|
||||
Your language is not there? Request it on our Crowdin page 😊 or ping us on [Matrix](https://matrix.to/#/#docs-official:matrix.org) and let us know if you can help with translations and/or proofreading.
|
||||
|
||||
## Creating an Issue
|
||||
|
||||
@@ -35,10 +35,14 @@ All commit messages must adhere to the following format:
|
||||
|
||||
`<gitmoji>(type) title description`
|
||||
|
||||
* <**gitmoji**>: Use a gitmoji to represent the purpose of the commit. For example, ✨ for adding a new feature or 🔥 for removing something, see the list here: <https://gitmoji.dev/>.
|
||||
* <**gitmoji**>: Use a gitmoji to represent the purpose of the commit. For example, ✨ for adding a new feature or 🔥 for removing something, see the list [here](https://gitmoji.dev/).
|
||||
* **(type)**: Describe the type of change. Common types include `backend`, `frontend`, `CI`, `docker` etc...
|
||||
* **title**: A short, descriptive title for the change.
|
||||
* **description**: Include additional details about what was changed and why.
|
||||
* **title**: A short, descriptive title for the change (*)
|
||||
* **blank line after the commit title
|
||||
* **description**: Include additional details on why you made the changes (**).
|
||||
|
||||
(*) ⚠️ **Make sure you add no space between the emoji and the (type) but add a space after the closing parenthesis of the type and use no caps!**
|
||||
(**) ⚠️ **Commit description message is mandatory and shouldn't be too long**
|
||||
|
||||
### Example Commit Message
|
||||
|
||||
@@ -66,7 +70,9 @@ Please add a line to the changelog describing your development. The changelog en
|
||||
It is nice to add information about the purpose of the pull request to help reviewers understand the context and intent of the changes. If you can, add some pictures or a small video to show the changes.
|
||||
|
||||
### Don't forget to:
|
||||
- check your commits
|
||||
- signoff your commits
|
||||
- sign your commits with your key (SSH, GPG etc.)
|
||||
- check your commits (see warnings above)
|
||||
- check the linting: `make lint && make frontend-lint`
|
||||
- check the tests: `make test`
|
||||
- add a changelog entry
|
||||
@@ -86,3 +92,11 @@ Make sure that all new features or fixes have corresponding tests. Run the test
|
||||
If you need any help while contributing, feel free to open a discussion or ask for guidance in the issue tracker. We are more than happy to assist!
|
||||
|
||||
Thank you for your contributions! 👍
|
||||
|
||||
## Contribute to BlockNote
|
||||
We use [BlockNote](https://www.blocknotejs.org/) for the text editing features of Docs.
|
||||
If you find and issue with the editor you can [report it](https://github.com/TypeCellOS/BlockNote/issues) directly on their repository.
|
||||
|
||||
Please consider contributing to BlockNotejs, as a library, it's useful to many projects not just Docs.
|
||||
|
||||
The project is licended with Mozilla Public License Version 2.0 but be aware that [XL packages](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-docx-exporter/LICENSE) are dual licenced with GNU AFFERO GENERAL PUBLIC LICENCE Version 3 and proprietary licence if you are [sponsor](https://www.blocknotejs.org/pricing).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Django impress
|
||||
|
||||
# ---- base image to inherit from ----
|
||||
FROM python:3.12.6-alpine3.20 AS base
|
||||
FROM python:3.12.10-alpine AS base
|
||||
|
||||
# Upgrade pip to its latest release to speed up dependencies installation
|
||||
RUN python -m pip install --upgrade pip setuptools
|
||||
@@ -30,12 +30,13 @@ RUN mkdir /install && \
|
||||
|
||||
|
||||
# ---- mails ----
|
||||
FROM node:20 AS mail-builder
|
||||
FROM node:24-alpine AS mail-builder
|
||||
|
||||
COPY ./src/mail /mail/app
|
||||
|
||||
WORKDIR /mail/app
|
||||
|
||||
RUN apk update && apk add --no-cache bash
|
||||
RUN yarn install --frozen-lockfile && \
|
||||
yarn build
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Security is very important to us.
|
||||
|
||||
If you have any issue regarding security, please disclose the information responsibly submiting [this form](https://vdp.numerique.gouv.fr/p/Send-a-report?lang=en) and not by creating an issue on the repository. You can also email us at docs@numerique.gouv.fr
|
||||
If you have any issue regarding security, please disclose the information responsibly submitting [this form](https://vdp.numerique.gouv.fr/p/Send-a-report?lang=en) and not by creating an issue on the repository. You can also email us at docs@numerique.gouv.fr
|
||||
|
||||
We appreciate your effort to make Docs more secure.
|
||||
|
||||
|
||||
@@ -155,8 +155,7 @@ services:
|
||||
target: frontend-production
|
||||
args:
|
||||
API_ORIGIN: "http://localhost:8071"
|
||||
Y_PROVIDER_URL: "ws://localhost:4444"
|
||||
MEDIA_URL: "http://localhost:8083"
|
||||
PUBLISH_AS_MIT: "false"
|
||||
SW_DEACTIVATED: "true"
|
||||
image: impress:frontend-development
|
||||
ports:
|
||||
@@ -185,15 +184,11 @@ services:
|
||||
context: .
|
||||
dockerfile: ./src/frontend/servers/y-provider/Dockerfile
|
||||
target: y-provider
|
||||
command: ["yarn", "workspace", "server-y-provider", "run", "dev"]
|
||||
working_dir: /app/frontend
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- env.d/development/common
|
||||
ports:
|
||||
- "4444:4444"
|
||||
volumes:
|
||||
- ./src/frontend/:/app/frontend
|
||||
|
||||
kc_postgresql:
|
||||
image: postgres:14.3
|
||||
|
||||
30
docs/env.md
30
docs/env.md
@@ -4,7 +4,7 @@ Here we describe all environment variables that can be set for the docs applicat
|
||||
|
||||
## impress-backend container
|
||||
|
||||
These are the environmental variables you can set for the impress-backend container.
|
||||
These are the environment variables you can set for the `impress-backend` container.
|
||||
|
||||
| Option | Description | default |
|
||||
| ----------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
@@ -39,7 +39,7 @@ These are the environmental 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 adress 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 | [] |
|
||||
@@ -49,9 +49,6 @@ These are the environmental variables you can set for the impress-backend contai
|
||||
| COLLABORATION_WS_URL | collaboration websocket url | |
|
||||
| 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_FOOTER_FEATURE_ENABLED | frontend feature flag to display the footer | false |
|
||||
| FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT | Cache duration of the json footer | 86400 |
|
||||
| FRONTEND_URL_JSON_FOOTER | Url with a json to configure the footer | |
|
||||
| FRONTEND_THEME | frontend theme to use | |
|
||||
| POSTHOG_KEY | posthog key for analytics | |
|
||||
| CRISP_WEBSITE_ID | crisp website id for support | |
|
||||
@@ -62,11 +59,11 @@ These are the environmental variables you can set for the impress-backend contai
|
||||
| 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 | Autorization 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 | |
|
||||
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth paramaters | {} |
|
||||
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth parameters | {} |
|
||||
| OIDC_RP_SCOPES | scopes requested for OIDC | openid email |
|
||||
| LOGIN_REDIRECT_URL | login redirect url | |
|
||||
| LOGIN_REDIRECT_URL_FAILURE | login redirect url on failure | |
|
||||
@@ -76,7 +73,7 @@ These are the environmental 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 dupplicate 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 |
|
||||
@@ -85,6 +82,7 @@ These are the environmental variables you can set for the impress-backend contai
|
||||
| AI_BASE_URL | OpenAI compatible AI base url | |
|
||||
| AI_MODEL | AI Model to use | |
|
||||
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
|
||||
| AI_FEATURE_ENABLED | Enable AI options | false |
|
||||
| Y_PROVIDER_API_KEY | Y provider API key | |
|
||||
| Y_PROVIDER_API_BASE_URL | Y Provider url | |
|
||||
| CONVERSION_API_ENDPOINT | Conversion API endpoint | convert-markdown |
|
||||
@@ -97,3 +95,19 @@ These are the environmental variables you can set for the impress-backend contai
|
||||
| DJANGO_CSRF_TRUSTED_ORIGINS | CSRF trusted origins | [] |
|
||||
| REDIS_URL | cache url | redis://redis:6379/1 |
|
||||
| CACHES_DEFAULT_TIMEOUT | cache default timeout | 30 |
|
||||
| CACHES_KEY_PREFIX | The prefix used to every cache keys. | docs |
|
||||
| MALWARE_DETECTION_BACKEND | The malware detection backend use from the django-lasuite package | lasuite.malware_detection.backends.dummy.DummyBackend |
|
||||
| MALWARE_DETECTION_PARAMETERS | A dict containing all the parameters to initiate the malware detection backend | {"callback_path": "core.malware_detection.malware_detection_callback",} |
|
||||
| THEME_CUSTOMIZATION_FILE_PATH | full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json |
|
||||
| THEME_CUSTOMIZATION_CACHE_TIMEOUT | Cache duration for the customization settings | 86400 |
|
||||
|
||||
|
||||
## impress-frontend image
|
||||
|
||||
These are the environment variables you can set to build the `impress-frontend` image.
|
||||
|
||||
| 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 |
|
||||
@@ -82,13 +82,13 @@ backend:
|
||||
python manage.py createsuperuser --email admin@example.com --password admin
|
||||
restartPolicy: Never
|
||||
|
||||
# Exra volume to manage our local custom CA and avoid to set ssl_verify: false
|
||||
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
|
||||
extraVolumeMounts:
|
||||
- name: certs
|
||||
mountPath: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
|
||||
subPath: cacert.pem
|
||||
|
||||
# Exra volume to manage our local custom CA and avoid to set ssl_verify: false
|
||||
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
|
||||
extraVolumes:
|
||||
- name: certs
|
||||
configMap:
|
||||
|
||||
@@ -133,7 +133,7 @@ OIDC_RP_SCOPES: "openid email"
|
||||
|
||||
You can find these values in **examples/keycloak.values.yaml**
|
||||
|
||||
### Find redis server connexion values
|
||||
### Find redis server connection values
|
||||
|
||||
Docs needs a redis so we start by deploying one:
|
||||
|
||||
@@ -146,7 +146,7 @@ keycloak-postgresql-0 1/1 Running 0 26m
|
||||
redis-master-0 1/1 Running 0 35s
|
||||
```
|
||||
|
||||
### Find postgresql connexion values
|
||||
### Find postgresql connection values
|
||||
|
||||
Docs uses a postgresql database as backend, so if you have a provider, obtain the necessary information to use it. If you don't, you can install a postgresql testing environment as follow:
|
||||
|
||||
@@ -173,7 +173,7 @@ POSTGRES_USER: dinum
|
||||
POSTGRES_PASSWORD: pass
|
||||
```
|
||||
|
||||
### Find s3 bucket connexion values
|
||||
### Find s3 bucket connection values
|
||||
|
||||
Docs uses an s3 bucket to store documents, so if you have a provider obtain the necessary information to use it. If you don't, you can install a local minio testing environment as follow:
|
||||
|
||||
@@ -191,7 +191,7 @@ redis-master-0 1/1 Running 0 10m
|
||||
|
||||
## Deployment
|
||||
|
||||
Now you are ready to deploy Docs without AI. AI requires more dependencies (OpenAI API). To deploy Docs you need to provide all previous informations to the helm chart.
|
||||
Now you are ready to deploy Docs without AI. AI requires more dependencies (OpenAI API). To deploy Docs you need to provide all previous information to the helm chart.
|
||||
|
||||
```
|
||||
$ helm repo add impress https://suitenumerique.github.io/docs/
|
||||
|
||||
@@ -64,5 +64,3 @@ COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
|
||||
|
||||
# Frontend
|
||||
FRONTEND_THEME=default
|
||||
FRONTEND_FOOTER_FEATURE_ENABLED=True
|
||||
FRONTEND_URL_JSON_FOOTER=http://frontend:3000/contents/footer-demo.json
|
||||
|
||||
@@ -4,3 +4,4 @@ BURST_THROTTLE_RATES="200/minute"
|
||||
DJANGO_SERVER_TO_SERVER_API_TOKENS=test-e2e
|
||||
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
|
||||
@@ -11,14 +11,16 @@
|
||||
},
|
||||
{
|
||||
"groupName": "allowed django versions",
|
||||
"matchManagers": [
|
||||
"pep621"
|
||||
],
|
||||
"matchPackageNames": [
|
||||
"Django"
|
||||
],
|
||||
"matchManagers": ["pep621"],
|
||||
"matchPackageNames": ["Django"],
|
||||
"allowedVersions": "<5.2"
|
||||
},
|
||||
{
|
||||
"groupName": "allowed redis versions",
|
||||
"matchManagers": ["pep621"],
|
||||
"matchPackageNames": ["redis"],
|
||||
"allowedVersions": "<6.0.0"
|
||||
},
|
||||
{
|
||||
"enabled": false,
|
||||
"groupName": "ignored js dependencies",
|
||||
@@ -26,6 +28,7 @@
|
||||
"matchPackageNames": [
|
||||
"eslint",
|
||||
"fetch-mock",
|
||||
"prosemirror-model",
|
||||
"node",
|
||||
"node-fetch",
|
||||
"workbox-webpack-plugin"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""API endpoints"""
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from urllib.parse import unquote, urlparse
|
||||
@@ -9,6 +10,7 @@ from django.conf import settings
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.contrib.postgres.search import TrigramSimilarity
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db import connection, transaction
|
||||
@@ -16,14 +18,13 @@ 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.utils.decorators import method_decorator
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.text import capfirst, slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.cache import cache_page
|
||||
|
||||
import requests
|
||||
import rest_framework as drf
|
||||
from botocore.exceptions import ClientError
|
||||
from lasuite.malware_detection import malware_detection
|
||||
from rest_framework import filters, status, viewsets
|
||||
from rest_framework import response as drf_response
|
||||
from rest_framework.permissions import AllowAny
|
||||
@@ -32,7 +33,6 @@ from rest_framework.throttling import UserRateThrottle
|
||||
from core import authentication, enums, models
|
||||
from core.services.ai_services import AIService
|
||||
from core.services.collaboration_services import CollaborationService
|
||||
from core.services.config_services import get_footer_json
|
||||
from core.utils import extract_attachments, filter_descendants
|
||||
|
||||
from . import permissions, serializers, utils
|
||||
@@ -576,7 +576,7 @@ class DocumentViewSet(
|
||||
queryset, filter_data["is_favorite"]
|
||||
)
|
||||
|
||||
# Apply ordering only now that everyting is filtered and annotated
|
||||
# Apply ordering only now that everything is filtered and annotated
|
||||
queryset = filters.OrderingFilter().filter_queryset(
|
||||
self.request, queryset, self
|
||||
)
|
||||
@@ -889,7 +889,7 @@ class DocumentViewSet(
|
||||
)
|
||||
|
||||
# Compute cache for ancestors links to avoid many queries while computing
|
||||
# abilties for his documents in the tree!
|
||||
# abilities for his documents in the tree!
|
||||
ancestors_links.append(
|
||||
{"link_reach": ancestor.link_reach, "link_role": ancestor.link_role}
|
||||
)
|
||||
@@ -1156,7 +1156,10 @@ class DocumentViewSet(
|
||||
|
||||
# Prepare metadata for storage
|
||||
extra_args = {
|
||||
"Metadata": {"owner": str(request.user.id)},
|
||||
"Metadata": {
|
||||
"owner": str(request.user.id),
|
||||
"status": enums.DocumentAttachmentStatus.PROCESSING,
|
||||
},
|
||||
"ContentType": serializer.validated_data["content_type"],
|
||||
}
|
||||
file_unsafe = ""
|
||||
@@ -1188,6 +1191,8 @@ class DocumentViewSet(
|
||||
document.attachments.append(key)
|
||||
document.save()
|
||||
|
||||
malware_detection.analyse_file(key, document_id=document.id)
|
||||
|
||||
return drf.response.Response(
|
||||
{"file": f"{settings.MEDIA_URL:s}{key:s}"},
|
||||
status=drf.status.HTTP_201_CREATED,
|
||||
@@ -1271,6 +1276,19 @@ class DocumentViewSet(
|
||||
logger.debug("User '%s' lacks permission for attachment", user)
|
||||
raise drf.exceptions.PermissionDenied()
|
||||
|
||||
# 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)
|
||||
metadata = head_resp.get("Metadata", {})
|
||||
# In order to be compatible with existing upload without `status` metadata,
|
||||
# we consider them as ready.
|
||||
if (
|
||||
metadata.get("status", enums.DocumentAttachmentStatus.READY)
|
||||
!= enums.DocumentAttachmentStatus.READY
|
||||
):
|
||||
raise drf.exceptions.PermissionDenied()
|
||||
|
||||
# Generate S3 authorization headers using the extracted URL parameters
|
||||
request = utils.generate_s3_authorization_headers(key)
|
||||
|
||||
@@ -1715,7 +1733,6 @@ class ConfigView(drf.views.APIView):
|
||||
"ENVIRONMENT",
|
||||
"FRONTEND_CSS_URL",
|
||||
"FRONTEND_HOMEPAGE_FEATURE_ENABLED",
|
||||
"FRONTEND_FOOTER_FEATURE_ENABLED",
|
||||
"FRONTEND_THEME",
|
||||
"MEDIA_BASE_URL",
|
||||
"POSTHOG_KEY",
|
||||
@@ -1728,23 +1745,41 @@ class ConfigView(drf.views.APIView):
|
||||
if hasattr(settings, setting):
|
||||
dict_settings[setting] = getattr(settings, setting)
|
||||
|
||||
dict_settings["theme_customization"] = self._load_theme_customization()
|
||||
|
||||
return drf.response.Response(dict_settings)
|
||||
|
||||
def _load_theme_customization(self):
|
||||
if not settings.THEME_CUSTOMIZATION_FILE_PATH:
|
||||
return {}
|
||||
|
||||
class FooterView(drf.views.APIView):
|
||||
"""API ViewSet for sharing the footer JSON."""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@method_decorator(cache_page(settings.FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT))
|
||||
def get(self, request):
|
||||
"""
|
||||
GET /api/v1.0/footer/
|
||||
Return the footer JSON.
|
||||
"""
|
||||
json_footer = (
|
||||
get_footer_json(settings.FRONTEND_URL_JSON_FOOTER)
|
||||
if settings.FRONTEND_URL_JSON_FOOTER
|
||||
else {}
|
||||
cache_key = (
|
||||
f"theme_customization_{slugify(settings.THEME_CUSTOMIZATION_FILE_PATH)}"
|
||||
)
|
||||
return drf.response.Response(json_footer)
|
||||
theme_customization = cache.get(cache_key, {})
|
||||
if theme_customization:
|
||||
return theme_customization
|
||||
|
||||
try:
|
||||
with open(
|
||||
settings.THEME_CUSTOMIZATION_FILE_PATH, "r", encoding="utf-8"
|
||||
) as f:
|
||||
theme_customization = json.load(f)
|
||||
except FileNotFoundError:
|
||||
logger.error(
|
||||
"Configuration file not found: %s",
|
||||
settings.THEME_CUSTOMIZATION_FILE_PATH,
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
logger.error(
|
||||
"Configuration file is not a valid JSON: %s",
|
||||
settings.THEME_CUSTOMIZATION_FILE_PATH,
|
||||
)
|
||||
else:
|
||||
cache.set(
|
||||
cache_key,
|
||||
theme_customization,
|
||||
settings.THEME_CUSTOMIZATION_CACHE_TIMEOUT,
|
||||
)
|
||||
|
||||
return theme_customization
|
||||
|
||||
@@ -3,6 +3,7 @@ Core application enums declaration
|
||||
"""
|
||||
|
||||
import re
|
||||
from enum import StrEnum
|
||||
|
||||
from django.conf import global_settings, settings
|
||||
from django.db import models
|
||||
@@ -38,3 +39,10 @@ class MoveNodePositionChoices(models.TextChoices):
|
||||
LAST_SIBLING = "last-sibling", _("Last sibling")
|
||||
LEFT = "left", _("Left")
|
||||
RIGHT = "right", _("Right")
|
||||
|
||||
|
||||
class DocumentAttachmentStatus(StrEnum):
|
||||
"""Defines the possible statuses for an attachment."""
|
||||
|
||||
PROCESSING = "processing"
|
||||
READY = "ready"
|
||||
|
||||
52
src/backend/core/malware_detection.py
Normal file
52
src/backend/core/malware_detection.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Malware detection callbacks"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
from lasuite.malware_detection.enums import ReportStatus
|
||||
|
||||
from core.enums import DocumentAttachmentStatus
|
||||
from core.models import Document
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
security_logger = logging.getLogger("docs.security")
|
||||
|
||||
|
||||
def malware_detection_callback(file_path, status, error_info, **kwargs):
|
||||
"""Malware detection callback"""
|
||||
|
||||
if status == ReportStatus.SAFE:
|
||||
logger.info("File %s is safe", file_path)
|
||||
# Get existing metadata
|
||||
s3_client = default_storage.connection.meta.client
|
||||
bucket_name = default_storage.bucket_name
|
||||
head_resp = s3_client.head_object(Bucket=bucket_name, Key=file_path)
|
||||
metadata = head_resp.get("Metadata", {})
|
||||
metadata.update({"status": DocumentAttachmentStatus.READY})
|
||||
# Update status in metadata
|
||||
s3_client.copy_object(
|
||||
Bucket=bucket_name,
|
||||
CopySource={"Bucket": bucket_name, "Key": file_path},
|
||||
Key=file_path,
|
||||
ContentType=head_resp.get("ContentType"),
|
||||
Metadata=metadata,
|
||||
MetadataDirective="REPLACE",
|
||||
)
|
||||
return
|
||||
|
||||
document_id = kwargs.get("document_id")
|
||||
security_logger.warning(
|
||||
"File %s for document %s is infected with malware. Error info: %s",
|
||||
file_path,
|
||||
document_id,
|
||||
error_info,
|
||||
)
|
||||
|
||||
# Remove the file from the document and change the status to unsafe
|
||||
document = Document.objects.get(pk=document_id)
|
||||
document.attachments.remove(file_path)
|
||||
document.save(update_fields=["attachments"])
|
||||
|
||||
# Delete the file from the storage
|
||||
default_storage.delete(file_path)
|
||||
@@ -44,7 +44,7 @@ AI_ACTIONS = {
|
||||
}
|
||||
|
||||
AI_TRANSLATE = (
|
||||
"Keep the same html stucture and formatting. "
|
||||
"Keep the same html structure and formatting. "
|
||||
"Translate the content in the html to the specified language {language:s}. "
|
||||
"Check the translation for accuracy and make any necessary corrections. "
|
||||
"Do not provide any other information."
|
||||
|
||||
@@ -17,7 +17,7 @@ class CollaborationService:
|
||||
def reset_connections(self, room, user_id=None):
|
||||
"""
|
||||
Reset connections of a room in the collaboration server.
|
||||
Reseting a connection means that the user will be disconnected and will
|
||||
Resetting a connection means that the user will be disconnected and will
|
||||
have to reconnect to the collaboration server, with updated rights.
|
||||
"""
|
||||
endpoint = "reset-connections"
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Config services."""
|
||||
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_footer_json(footer_json_url: str) -> dict:
|
||||
"""
|
||||
Fetches the footer JSON from the given URL."
|
||||
"""
|
||||
try:
|
||||
response = requests.get(
|
||||
footer_json_url, timeout=5, headers={"User-Agent": "Docs-Application"}
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
footer_json = response.json()
|
||||
|
||||
return footer_json
|
||||
except (requests.RequestException, ValueError) as e:
|
||||
logger.error("Failed to fetch footer JSON: %s", e)
|
||||
return {}
|
||||
@@ -575,7 +575,7 @@ def test_api_document_invitations_create_cannot_invite_existing_users():
|
||||
document = factories.DocumentFactory(users=[(user, "owner")])
|
||||
existing_user = factories.UserFactory()
|
||||
|
||||
# Build an invitation to the email of an exising identity in the db
|
||||
# Build an invitation to the email of an existing identity in the db
|
||||
invitation_values = {
|
||||
"email": existing_user.email,
|
||||
"role": random.choice(models.RoleChoices.values),
|
||||
|
||||
@@ -150,7 +150,7 @@ def test_api_documents_ai_transform_authenticated_forbidden(reach, role):
|
||||
@patch("openai.resources.chat.completions.Completions.create")
|
||||
def test_api_documents_ai_transform_authenticated_success(mock_create, reach, role):
|
||||
"""
|
||||
Autenticated who are not related to a document should be able to request AI transform
|
||||
Authenticated who are not related to a document should be able to request AI transform
|
||||
if the link reach and role permit it.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -99,7 +99,7 @@ def test_api_documents_ai_translate_anonymous_success(mock_create):
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Keep the same html stucture and formatting. "
|
||||
"Keep the same html structure and formatting. "
|
||||
"Translate the content in the html to the specified language Spanish. "
|
||||
"Check the translation for accuracy and make any necessary corrections. "
|
||||
"Do not provide any other information."
|
||||
@@ -172,7 +172,7 @@ def test_api_documents_ai_translate_authenticated_forbidden(reach, role):
|
||||
@patch("openai.resources.chat.completions.Completions.create")
|
||||
def test_api_documents_ai_translate_authenticated_success(mock_create, reach, role):
|
||||
"""
|
||||
Autenticated who are not related to a document should be able to request AI translate
|
||||
Authenticated who are not related to a document should be able to request AI translate
|
||||
if the link reach and role permit it.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
@@ -197,7 +197,7 @@ def test_api_documents_ai_translate_authenticated_success(mock_create, reach, ro
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Keep the same html stucture and formatting. "
|
||||
"Keep the same html structure and formatting. "
|
||||
"Translate the content in the html to the "
|
||||
"specified language Colombian Spanish. "
|
||||
"Check the translation for accuracy and make any necessary corrections. "
|
||||
@@ -274,7 +274,7 @@ def test_api_documents_ai_translate_success(mock_create, via, role, mock_user_te
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Keep the same html stucture and formatting. "
|
||||
"Keep the same html structure and formatting. "
|
||||
"Translate the content in the html to the "
|
||||
"specified language Colombian Spanish. "
|
||||
"Check the translation for accuracy and make any necessary corrections. "
|
||||
|
||||
@@ -4,6 +4,7 @@ Test file uploads API endpoint for users in impress's core app.
|
||||
|
||||
import re
|
||||
import uuid
|
||||
from unittest import mock
|
||||
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
@@ -12,6 +13,7 @@ import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
from core.api.viewsets import malware_detection
|
||||
from core.tests.conftest import TEAM, USER, VIA
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
@@ -59,7 +61,8 @@ def test_api_documents_attachment_upload_anonymous_success():
|
||||
file = SimpleUploadedFile(name="test.png", content=PIXEL, content_type="image/png")
|
||||
|
||||
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
|
||||
response = APIClient().post(url, {"file": file}, format="multipart")
|
||||
with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file:
|
||||
response = APIClient().post(url, {"file": file}, format="multipart")
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
@@ -74,12 +77,13 @@ def test_api_documents_attachment_upload_anonymous_success():
|
||||
assert document.attachments == [f"{document.id!s}/attachments/{file_id!s}.png"]
|
||||
|
||||
# Now, check the metadata of the uploaded file
|
||||
key = file_path.replace("/media", "")
|
||||
key = file_path.replace("/media/", "")
|
||||
mock_analyse_file.assert_called_once_with(key, document_id=document.id)
|
||||
file_head = default_storage.connection.meta.client.head_object(
|
||||
Bucket=default_storage.bucket_name, Key=key
|
||||
)
|
||||
|
||||
assert file_head["Metadata"] == {"owner": "None"}
|
||||
assert file_head["Metadata"] == {"owner": "None", "status": "processing"}
|
||||
assert file_head["ContentType"] == "image/png"
|
||||
assert file_head["ContentDisposition"] == 'inline; filename="test.png"'
|
||||
|
||||
@@ -127,7 +131,7 @@ def test_api_documents_attachment_upload_authenticated_forbidden(reach, role):
|
||||
)
|
||||
def test_api_documents_attachment_upload_authenticated_success(reach, role):
|
||||
"""
|
||||
Autenticated users who are not related to a document should be able to upload
|
||||
Authenticated users who are not related to a document should be able to upload
|
||||
a file when the link reach and role permit it.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
@@ -139,7 +143,8 @@ def test_api_documents_attachment_upload_authenticated_success(reach, role):
|
||||
file = SimpleUploadedFile(name="test.png", content=PIXEL, content_type="image/png")
|
||||
|
||||
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
|
||||
response = client.post(url, {"file": file}, format="multipart")
|
||||
with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file:
|
||||
response = client.post(url, {"file": file}, format="multipart")
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
@@ -147,6 +152,10 @@ def test_api_documents_attachment_upload_authenticated_success(reach, role):
|
||||
match = pattern.search(response.json()["file"])
|
||||
file_id = match.group(1)
|
||||
|
||||
mock_analyse_file.assert_called_once_with(
|
||||
f"{document.id!s}/attachments/{file_id!s}.png", document_id=document.id
|
||||
)
|
||||
|
||||
# Validate that file_id is a valid UUID
|
||||
uuid.UUID(file_id)
|
||||
|
||||
@@ -210,7 +219,8 @@ def test_api_documents_attachment_upload_success(via, role, mock_user_teams):
|
||||
file = SimpleUploadedFile(name="test.png", content=PIXEL, content_type="image/png")
|
||||
|
||||
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
|
||||
response = client.post(url, {"file": file}, format="multipart")
|
||||
with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file:
|
||||
response = client.post(url, {"file": file}, format="multipart")
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
@@ -226,11 +236,12 @@ def test_api_documents_attachment_upload_success(via, role, mock_user_teams):
|
||||
assert document.attachments == [f"{document.id!s}/attachments/{file_id!s}.png"]
|
||||
|
||||
# Now, check the metadata of the uploaded file
|
||||
key = file_path.replace("/media", "")
|
||||
key = file_path.replace("/media/", "")
|
||||
mock_analyse_file.assert_called_once_with(key, document_id=document.id)
|
||||
file_head = default_storage.connection.meta.client.head_object(
|
||||
Bucket=default_storage.bucket_name, Key=key
|
||||
)
|
||||
assert file_head["Metadata"] == {"owner": str(user.id)}
|
||||
assert file_head["Metadata"] == {"owner": str(user.id), "status": "processing"}
|
||||
assert file_head["ContentType"] == "image/png"
|
||||
assert file_head["ContentDisposition"] == 'inline; filename="test.png"'
|
||||
|
||||
@@ -255,7 +266,7 @@ def test_api_documents_attachment_upload_invalid(client):
|
||||
|
||||
|
||||
def test_api_documents_attachment_upload_size_limit_exceeded(settings):
|
||||
"""The uploaded file should not exceeed the maximum size in settings."""
|
||||
"""The uploaded file should not exceed the maximum size in settings."""
|
||||
settings.DOCUMENT_IMAGE_MAX_SIZE = 1048576 # 1 MB for test
|
||||
|
||||
user = factories.UserFactory()
|
||||
@@ -304,7 +315,8 @@ def test_api_documents_attachment_upload_fix_extension(
|
||||
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
|
||||
|
||||
file = SimpleUploadedFile(name=name, content=content)
|
||||
response = client.post(url, {"file": file}, format="multipart")
|
||||
with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file:
|
||||
response = client.post(url, {"file": file}, format="multipart")
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
@@ -324,11 +336,16 @@ def test_api_documents_attachment_upload_fix_extension(
|
||||
uuid.UUID(file_id)
|
||||
|
||||
# Now, check the metadata of the uploaded file
|
||||
key = file_path.replace("/media", "")
|
||||
key = file_path.replace("/media/", "")
|
||||
mock_analyse_file.assert_called_once_with(key, document_id=document.id)
|
||||
file_head = default_storage.connection.meta.client.head_object(
|
||||
Bucket=default_storage.bucket_name, Key=key
|
||||
)
|
||||
assert file_head["Metadata"] == {"owner": str(user.id), "is_unsafe": "true"}
|
||||
assert file_head["Metadata"] == {
|
||||
"owner": str(user.id),
|
||||
"is_unsafe": "true",
|
||||
"status": "processing",
|
||||
}
|
||||
assert file_head["ContentType"] == content_type
|
||||
assert file_head["ContentDisposition"] == f'attachment; filename="{name:s}"'
|
||||
|
||||
@@ -364,7 +381,8 @@ def test_api_documents_attachment_upload_unsafe():
|
||||
file = SimpleUploadedFile(
|
||||
name="script.exe", content=b"\x4d\x5a\x90\x00\x03\x00\x00\x00"
|
||||
)
|
||||
response = client.post(url, {"file": file}, format="multipart")
|
||||
with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file:
|
||||
response = client.post(url, {"file": file}, format="multipart")
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
@@ -381,11 +399,16 @@ def test_api_documents_attachment_upload_unsafe():
|
||||
file_id = file_id.replace("-unsafe", "")
|
||||
uuid.UUID(file_id)
|
||||
|
||||
key = file_path.replace("/media/", "")
|
||||
mock_analyse_file.assert_called_once_with(key, document_id=document.id)
|
||||
# Now, check the metadata of the uploaded file
|
||||
key = file_path.replace("/media", "")
|
||||
file_head = default_storage.connection.meta.client.head_object(
|
||||
Bucket=default_storage.bucket_name, Key=key
|
||||
)
|
||||
assert file_head["Metadata"] == {"owner": str(user.id), "is_unsafe": "true"}
|
||||
assert file_head["Metadata"] == {
|
||||
"owner": str(user.id),
|
||||
"is_unsafe": "true",
|
||||
"status": "processing",
|
||||
}
|
||||
assert file_head["ContentType"] == "application/octet-stream"
|
||||
assert file_head["ContentDisposition"] == 'attachment; filename="script.exe"'
|
||||
|
||||
@@ -279,7 +279,7 @@ def test_api_documents_create_for_owner_existing_user_email_no_sub_with_fallback
|
||||
"""
|
||||
It should be possible to create a document on behalf of a pre-existing user for
|
||||
who the sub was not found if the settings allow it. This edge case should not
|
||||
happen in a healthy OIDC federation but can be usefull if an OIDC provider modifies
|
||||
happen in a healthy OIDC federation but can be useful if an OIDC provider modifies
|
||||
users sub on each login for example...
|
||||
"""
|
||||
user = factories.UserFactory(language="en-us")
|
||||
|
||||
@@ -15,6 +15,7 @@ import requests
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
from core.enums import DocumentAttachmentStatus
|
||||
from core.tests.conftest import TEAM, USER, VIA
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
@@ -45,6 +46,7 @@ def test_api_documents_media_auth_anonymous_public():
|
||||
Key=key,
|
||||
Body=BytesIO(b"my prose"),
|
||||
ContentType="text/plain",
|
||||
Metadata={"status": DocumentAttachmentStatus.READY},
|
||||
)
|
||||
|
||||
factories.DocumentFactory(id=document_id, link_reach="public", attachments=[key])
|
||||
@@ -93,7 +95,15 @@ def test_api_documents_media_auth_extensions():
|
||||
keys = []
|
||||
for ext in extensions:
|
||||
filename = f"{uuid4()!s}.{ext:s}"
|
||||
keys.append(f"{document_id!s}/attachments/{filename:s}")
|
||||
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},
|
||||
)
|
||||
keys.append(key)
|
||||
|
||||
factories.DocumentFactory(link_reach="public", attachments=keys)
|
||||
|
||||
@@ -142,6 +152,7 @@ def test_api_documents_media_auth_anonymous_attachments():
|
||||
Key=key,
|
||||
Body=BytesIO(b"my prose"),
|
||||
ContentType="text/plain",
|
||||
Metadata={"status": DocumentAttachmentStatus.READY},
|
||||
)
|
||||
|
||||
factories.DocumentFactory(id=document_id, link_reach="restricted")
|
||||
@@ -205,6 +216,7 @@ def test_api_documents_media_auth_authenticated_public_or_authenticated(reach):
|
||||
Key=key,
|
||||
Body=BytesIO(b"my prose"),
|
||||
ContentType="text/plain",
|
||||
Metadata={"status": DocumentAttachmentStatus.READY},
|
||||
)
|
||||
|
||||
factories.DocumentFactory(id=document_id, link_reach=reach, attachments=[key])
|
||||
@@ -283,6 +295,7 @@ def test_api_documents_media_auth_related(via, mock_user_teams):
|
||||
Key=key,
|
||||
Body=BytesIO(b"my prose"),
|
||||
ContentType="text/plain",
|
||||
Metadata={"status": DocumentAttachmentStatus.READY},
|
||||
)
|
||||
|
||||
document = factories.DocumentFactory(
|
||||
@@ -321,3 +334,70 @@ def test_api_documents_media_auth_related(via, mock_user_teams):
|
||||
timeout=1,
|
||||
)
|
||||
assert response.content.decode("utf-8") == "my prose"
|
||||
|
||||
|
||||
def test_api_documents_media_auth_not_ready_status():
|
||||
"""Attachments with status not ready should not be accessible"""
|
||||
document_id = uuid4()
|
||||
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},
|
||||
)
|
||||
|
||||
factories.DocumentFactory(id=document_id, link_reach="public", attachments=[key])
|
||||
|
||||
original_url = f"http://localhost/media/{key:s}"
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_api_documents_media_auth_missing_status_metadata():
|
||||
"""Attachments without status metadata should be considered as ready"""
|
||||
document_id = uuid4()
|
||||
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",
|
||||
)
|
||||
|
||||
factories.DocumentFactory(id=document_id, link_reach="public", attachments=[key])
|
||||
|
||||
original_url = f"http://localhost/media/{key:s}"
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
authorization = response["Authorization"]
|
||||
assert "AWS4-HMAC-SHA256 Credential=" in authorization
|
||||
assert (
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
|
||||
in authorization
|
||||
)
|
||||
assert response["X-Amz-Date"] == timezone.now().strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
|
||||
file_url = f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{key:s}"
|
||||
response = requests.get(
|
||||
file_url,
|
||||
headers={
|
||||
"authorization": authorization,
|
||||
"x-amz-date": response["x-amz-date"],
|
||||
"x-amz-content-sha256": response["x-amz-content-sha256"],
|
||||
"Host": f"{s3_url.hostname:s}:{s3_url.port:d}",
|
||||
},
|
||||
timeout=1,
|
||||
)
|
||||
assert response.content.decode("utf-8") == "my prose"
|
||||
|
||||
@@ -310,7 +310,7 @@ def test_api_documents_move_authenticated_deleted_target_as_child(position):
|
||||
def test_api_documents_move_authenticated_deleted_target_as_sibling(position):
|
||||
"""
|
||||
It should not be possible to move a document as a sibling of a deleted target document
|
||||
if the user has no rigths on its parent.
|
||||
if the user has no rights on its parent.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
|
||||
@@ -31,7 +31,7 @@ def get_ydoc_with_mages(image_keys):
|
||||
def test_api_documents_update_new_attachment_keys_anonymous(django_assert_num_queries):
|
||||
"""
|
||||
When an anonymous user updates a document, the attachment keys extracted from the
|
||||
updated content should be added to the list of "attachments" ot the document if these
|
||||
updated content should be added to the list of "attachments" to the document if these
|
||||
attachments are already readable by anonymous users.
|
||||
"""
|
||||
image_keys = [f"{uuid4()!s}/attachments/{uuid4()!s}.png" for _ in range(4)]
|
||||
@@ -78,7 +78,7 @@ def test_api_documents_update_new_attachment_keys_authenticated(
|
||||
):
|
||||
"""
|
||||
When an authenticated user updates a document, the attachment keys extracted from the
|
||||
updated content should be added to the list of "attachments" ot the document if these
|
||||
updated content should be added to the list of "attachments" to the document if these
|
||||
attachments are already readable by the editing user.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Test config API endpoints in the Impress core app.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from django.test import override_settings
|
||||
|
||||
import pytest
|
||||
@@ -16,14 +18,15 @@ pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@override_settings(
|
||||
AI_FEATURE_ENABLED=False,
|
||||
COLLABORATION_WS_URL="http://testcollab/",
|
||||
CRISP_WEBSITE_ID="123",
|
||||
FRONTEND_CSS_URL="http://testcss/",
|
||||
FRONTEND_FOOTER_FEATURE_ENABLED=True,
|
||||
FRONTEND_THEME="test-theme",
|
||||
MEDIA_BASE_URL="http://testserver/",
|
||||
POSTHOG_KEY={"id": "132456", "host": "https://eu.i.posthog-test.com"},
|
||||
SENTRY_DSN="https://sentry.test/123",
|
||||
THEME_CUSTOMIZATION_FILE_PATH="",
|
||||
)
|
||||
@pytest.mark.parametrize("is_authenticated", [False, True])
|
||||
def test_api_config(is_authenticated):
|
||||
@@ -42,7 +45,6 @@ def test_api_config(is_authenticated):
|
||||
"ENVIRONMENT": "test",
|
||||
"FRONTEND_CSS_URL": "http://testcss/",
|
||||
"FRONTEND_HOMEPAGE_FEATURE_ENABLED": True,
|
||||
"FRONTEND_FOOTER_FEATURE_ENABLED": True,
|
||||
"FRONTEND_THEME": "test-theme",
|
||||
"LANGUAGES": [
|
||||
["en-us", "English"],
|
||||
@@ -56,4 +58,98 @@ def test_api_config(is_authenticated):
|
||||
"POSTHOG_KEY": {"id": "132456", "host": "https://eu.i.posthog-test.com"},
|
||||
"SENTRY_DSN": "https://sentry.test/123",
|
||||
"AI_FEATURE_ENABLED": False,
|
||||
"theme_customization": {},
|
||||
}
|
||||
|
||||
|
||||
@override_settings(
|
||||
THEME_CUSTOMIZATION_FILE_PATH="/not/existing/file.json",
|
||||
)
|
||||
@pytest.mark.parametrize("is_authenticated", [False, True])
|
||||
def test_api_config_with_invalid_theme_customization_file(is_authenticated):
|
||||
"""Anonymous users should be allowed to get the configuration."""
|
||||
client = APIClient()
|
||||
|
||||
if is_authenticated:
|
||||
user = factories.UserFactory()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get("/api/v1.0/config/")
|
||||
assert response.status_code == HTTP_200_OK
|
||||
content = response.json()
|
||||
assert content["theme_customization"] == {}
|
||||
|
||||
|
||||
@override_settings(
|
||||
THEME_CUSTOMIZATION_FILE_PATH="/configuration/theme/invalid.json",
|
||||
)
|
||||
@pytest.mark.parametrize("is_authenticated", [False, True])
|
||||
def test_api_config_with_invalid_json_theme_customization_file(is_authenticated, fs):
|
||||
"""Anonymous users should be allowed to get the configuration."""
|
||||
fs.create_file(
|
||||
"/configuration/theme/invalid.json",
|
||||
contents="invalid json",
|
||||
)
|
||||
client = APIClient()
|
||||
|
||||
if is_authenticated:
|
||||
user = factories.UserFactory()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get("/api/v1.0/config/")
|
||||
assert response.status_code == HTTP_200_OK
|
||||
content = response.json()
|
||||
assert content["theme_customization"] == {}
|
||||
|
||||
|
||||
@override_settings(
|
||||
THEME_CUSTOMIZATION_FILE_PATH="/configuration/theme/default.json",
|
||||
)
|
||||
@pytest.mark.parametrize("is_authenticated", [False, True])
|
||||
def test_api_config_with_theme_customization(is_authenticated, fs):
|
||||
"""Anonymous users should be allowed to get the configuration."""
|
||||
fs.create_file(
|
||||
"/configuration/theme/default.json",
|
||||
contents=json.dumps(
|
||||
{
|
||||
"colors": {
|
||||
"primary": "#000000",
|
||||
"secondary": "#000000",
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
client = APIClient()
|
||||
|
||||
if is_authenticated:
|
||||
user = factories.UserFactory()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get("/api/v1.0/config/")
|
||||
assert response.status_code == HTTP_200_OK
|
||||
content = response.json()
|
||||
assert content["theme_customization"] == {
|
||||
"colors": {
|
||||
"primary": "#000000",
|
||||
"secondary": "#000000",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_authenticated", [False, True])
|
||||
def test_api_config_with_original_theme_customization(is_authenticated, settings):
|
||||
"""Anonymous users should be allowed to get the configuration."""
|
||||
client = APIClient()
|
||||
|
||||
if is_authenticated:
|
||||
user = factories.UserFactory()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get("/api/v1.0/config/")
|
||||
assert response.status_code == HTTP_200_OK
|
||||
content = response.json()
|
||||
|
||||
with open(settings.THEME_CUSTOMIZATION_FILE_PATH, "r", encoding="utf-8") as f:
|
||||
theme_customization = json.load(f)
|
||||
|
||||
assert content["theme_customization"] == theme_customization
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
"""Test the footer API."""
|
||||
|
||||
import responses
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
def test_api_footer_without_settings_configured(settings):
|
||||
"""Test the footer API without settings configured."""
|
||||
settings.FRONTEND_URL_JSON_FOOTER = None
|
||||
client = APIClient()
|
||||
response = client.get("/api/v1.0/footer/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {}
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_footer_with_invalid_request(settings):
|
||||
"""Test the footer API with an invalid request."""
|
||||
settings.FRONTEND_URL_JSON_FOOTER = "https://invalid-request.com"
|
||||
|
||||
footer_response = responses.get(settings.FRONTEND_URL_JSON_FOOTER, status=404)
|
||||
|
||||
client = APIClient()
|
||||
response = client.get("/api/v1.0/footer/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {}
|
||||
assert footer_response.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_footer_with_invalid_json(settings):
|
||||
"""Test the footer API with an invalid JSON response."""
|
||||
settings.FRONTEND_URL_JSON_FOOTER = "https://valid-request.com"
|
||||
|
||||
footer_response = responses.get(
|
||||
settings.FRONTEND_URL_JSON_FOOTER, status=200, body="invalid json"
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
response = client.get("/api/v1.0/footer/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {}
|
||||
assert footer_response.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_footer_with_valid_json(settings):
|
||||
"""Test the footer API with an invalid JSON response."""
|
||||
settings.FRONTEND_URL_JSON_FOOTER = "https://valid-request.com"
|
||||
|
||||
footer_response = responses.get(
|
||||
settings.FRONTEND_URL_JSON_FOOTER, status=200, json={"foo": "bar"}
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
response = client.get("/api/v1.0/footer/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"foo": "bar"}
|
||||
assert footer_response.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_footer_with_valid_json_and_cache(settings):
|
||||
"""Test the footer API with an invalid JSON response."""
|
||||
settings.FRONTEND_URL_JSON_FOOTER = "https://valid-request.com"
|
||||
|
||||
footer_response = responses.get(
|
||||
settings.FRONTEND_URL_JSON_FOOTER, status=200, json={"foo": "bar"}
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
response = client.get("/api/v1.0/footer/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"foo": "bar"}
|
||||
assert footer_response.call_count == 1
|
||||
|
||||
response = client.get("/api/v1.0/footer/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"foo": "bar"}
|
||||
# The cache should have been used
|
||||
assert footer_response.call_count == 1
|
||||
76
src/backend/core/tests/test_malware_detection.py
Normal file
76
src/backend/core/tests/test_malware_detection.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""Test malware detection callback."""
|
||||
|
||||
import random
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
import pytest
|
||||
from lasuite.malware_detection.enums import ReportStatus
|
||||
|
||||
from core.enums import DocumentAttachmentStatus
|
||||
from core.factories import DocumentFactory
|
||||
from core.malware_detection import malware_detection_callback
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.fixture(name="safe_file")
|
||||
def fixture_safe_file():
|
||||
"""Create a safe file."""
|
||||
file_path = "test.txt"
|
||||
default_storage.save(file_path, ContentFile("test"))
|
||||
yield file_path
|
||||
default_storage.delete(file_path)
|
||||
|
||||
|
||||
@pytest.fixture(name="unsafe_file")
|
||||
def fixture_unsafe_file():
|
||||
"""Create an unsafe file."""
|
||||
file_path = "unsafe.txt"
|
||||
default_storage.save(file_path, ContentFile("test"))
|
||||
yield file_path
|
||||
|
||||
|
||||
def test_malware_detection_callback_safe_status(safe_file):
|
||||
"""Test malware detection callback with safe status."""
|
||||
|
||||
document = DocumentFactory(attachments=[safe_file])
|
||||
|
||||
malware_detection_callback(
|
||||
safe_file,
|
||||
ReportStatus.SAFE,
|
||||
error_info={},
|
||||
document_id=document.id,
|
||||
)
|
||||
|
||||
document.refresh_from_db()
|
||||
|
||||
assert safe_file in document.attachments
|
||||
assert default_storage.exists(safe_file)
|
||||
|
||||
s3_client = default_storage.connection.meta.client
|
||||
bucket_name = default_storage.bucket_name
|
||||
head_resp = s3_client.head_object(Bucket=bucket_name, Key=safe_file)
|
||||
metadata = head_resp.get("Metadata", {})
|
||||
assert metadata["status"] == DocumentAttachmentStatus.READY
|
||||
|
||||
|
||||
def test_malware_detection_callback_unsafe_status(unsafe_file):
|
||||
"""Test malware detection callback with unsafe status."""
|
||||
|
||||
document = DocumentFactory(attachments=[unsafe_file])
|
||||
|
||||
malware_detection_callback(
|
||||
unsafe_file,
|
||||
random.choice(
|
||||
[status.value for status in ReportStatus if status != ReportStatus.SAFE]
|
||||
),
|
||||
error_info={"error": "test", "error_code": 4001},
|
||||
document_id=document.id,
|
||||
)
|
||||
|
||||
document.refresh_from_db()
|
||||
|
||||
assert unsafe_file not in document.attachments
|
||||
assert not default_storage.exists(unsafe_file)
|
||||
@@ -56,5 +56,4 @@ urlpatterns = [
|
||||
),
|
||||
),
|
||||
path(f"api/{settings.API_VERSION}/config/", viewsets.ConfigView.as_view()),
|
||||
path(f"api/{settings.API_VERSION}/footer/", viewsets.FooterView.as_view()),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Impress package. Import the celery app early to load shared task form dependencies."""
|
||||
|
||||
from .celery_app import app as celery_app
|
||||
|
||||
__all__ = ["celery_app"]
|
||||
|
||||
@@ -11,6 +11,9 @@ os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
|
||||
|
||||
install(check_options=True)
|
||||
|
||||
# Can not be loaded only after install call.
|
||||
from django.conf import settings # pylint: disable=wrong-import-position
|
||||
|
||||
app = Celery("impress")
|
||||
|
||||
# Using a string here means the worker doesn't have to serialize
|
||||
@@ -20,4 +23,4 @@ app = Celery("impress")
|
||||
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||
|
||||
# Load task modules from all registered Django apps.
|
||||
app.autodiscover_tasks()
|
||||
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
|
||||
|
||||
124
src/backend/impress/configuration/theme/default.json
Normal file
124
src/backend/impress/configuration/theme/default.json
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +317,7 @@ class Base(Configuration):
|
||||
"django.contrib.staticfiles",
|
||||
# OIDC third party
|
||||
"mozilla_django_oidc",
|
||||
"lasuite.malware_detection",
|
||||
]
|
||||
|
||||
# Cache
|
||||
@@ -422,23 +423,22 @@ class Base(Configuration):
|
||||
environ_name="FRONTEND_HOMEPAGE_FEATURE_ENABLED",
|
||||
environ_prefix=None,
|
||||
)
|
||||
FRONTEND_URL_JSON_FOOTER = values.Value(
|
||||
None, environ_name="FRONTEND_URL_JSON_FOOTER", environ_prefix=None
|
||||
)
|
||||
FRONTEND_FOOTER_FEATURE_ENABLED = values.BooleanValue(
|
||||
default=False,
|
||||
environ_name="FRONTEND_FOOTER_FEATURE_ENABLED",
|
||||
environ_prefix=None,
|
||||
)
|
||||
FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT = values.Value(
|
||||
60 * 60 * 24,
|
||||
environ_name="FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT",
|
||||
environ_prefix=None,
|
||||
)
|
||||
FRONTEND_CSS_URL = values.Value(
|
||||
None, environ_name="FRONTEND_CSS_URL", environ_prefix=None
|
||||
)
|
||||
|
||||
THEME_CUSTOMIZATION_FILE_PATH = values.Value(
|
||||
os.path.join(BASE_DIR, "impress/configuration/theme/default.json"),
|
||||
environ_name="THEME_CUSTOMIZATION_FILE_PATH",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
THEME_CUSTOMIZATION_CACHE_TIMEOUT = values.Value(
|
||||
60 * 60 * 24,
|
||||
environ_name="THEME_CUSTOMIZATION_CACHE_TIMEOUT",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Posthog
|
||||
POSTHOG_KEY = values.DictValue(
|
||||
None, environ_name="POSTHOG_KEY", environ_prefix=None
|
||||
@@ -680,6 +680,21 @@ class Base(Configuration):
|
||||
},
|
||||
}
|
||||
|
||||
MALWARE_DETECTION = {
|
||||
"BACKEND": values.Value(
|
||||
"lasuite.malware_detection.backends.dummy.DummyBackend",
|
||||
environ_name="MALWARE_DETECTION_BACKEND",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"PARAMETERS": values.DictValue(
|
||||
default={
|
||||
"callback_path": "core.malware_detection.malware_detection_callback",
|
||||
},
|
||||
environ_name="MALWARE_DETECTION_PARAMETERS",
|
||||
environ_prefix=None,
|
||||
),
|
||||
}
|
||||
|
||||
API_USERS_LIST_LIMIT = values.PositiveIntegerValue(
|
||||
default=5,
|
||||
environ_name="API_USERS_LIST_LIMIT",
|
||||
@@ -898,6 +913,11 @@ class Production(Base):
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
},
|
||||
"KEY_PREFIX": values.Value(
|
||||
"docs",
|
||||
environ_name="CACHES_KEY_PREFIX",
|
||||
environ_prefix=None,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "3.2.0"
|
||||
version = "3.2.1"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -25,21 +25,20 @@ license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"beautifulsoup4==4.13.3",
|
||||
"boto3==1.37.33",
|
||||
"beautifulsoup4==4.13.4",
|
||||
"boto3==1.38.13",
|
||||
"Brotli==1.1.0",
|
||||
"celery[redis]==5.5.1",
|
||||
"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==0.0.7",
|
||||
"django-lasuite[all]==0.0.8",
|
||||
"django-parler==2.3",
|
||||
"redis==5.2.1",
|
||||
"django-redis==5.4.0",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django==5.1.8",
|
||||
"django==5.1.9",
|
||||
"django-treebeard==4.7.1",
|
||||
"djangorestframework==3.16.0",
|
||||
"drf_spectacular==0.28.0",
|
||||
@@ -48,17 +47,18 @@ dependencies = [
|
||||
"factory_boy==3.3.3",
|
||||
"gunicorn==23.0.0",
|
||||
"jsonschema==4.23.0",
|
||||
"lxml==5.3.2",
|
||||
"lxml==5.4.0",
|
||||
"markdown==3.8",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
"nested-multipart-parser==1.5.0",
|
||||
"openai==1.73.0",
|
||||
"psycopg[binary]==3.2.6",
|
||||
"pycrdt==0.12.12",
|
||||
"openai==1.78.1",
|
||||
"psycopg[binary]==3.2.8",
|
||||
"pycrdt==0.12.15",
|
||||
"PyJWT==2.10.1",
|
||||
"python-magic==0.4.27",
|
||||
"redis<6.0.0",
|
||||
"requests==2.32.3",
|
||||
"sentry-sdk==2.25.1",
|
||||
"sentry-sdk==2.28.0",
|
||||
"whitenoise==6.9.0",
|
||||
]
|
||||
|
||||
@@ -71,21 +71,21 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"django-extensions==4.1",
|
||||
"django-test-migrations==1.4.0",
|
||||
"drf-spectacular-sidecar==2025.4.1",
|
||||
"django-test-migrations==1.5.0",
|
||||
"drf-spectacular-sidecar==2025.5.1",
|
||||
"freezegun==1.5.1",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==9.1.0",
|
||||
"ipython==9.2.0",
|
||||
"pyfakefs==5.8.0",
|
||||
"pylint-django==2.6.1",
|
||||
"pylint==3.3.6",
|
||||
"pylint==3.3.7",
|
||||
"pytest-cov==6.1.1",
|
||||
"pytest-django==4.11.1",
|
||||
"pytest==8.3.5",
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.6.1",
|
||||
"responses==0.25.7",
|
||||
"ruff==0.11.5",
|
||||
"ruff==0.11.9",
|
||||
"types-requests==2.32.0.20250328",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine AS frontend-deps
|
||||
FROM node:24-alpine AS frontend-deps
|
||||
|
||||
WORKDIR /home/frontend/
|
||||
|
||||
@@ -39,10 +39,13 @@ ENV NEXT_PUBLIC_API_ORIGIN=${API_ORIGIN}
|
||||
ARG SW_DEACTIVATED
|
||||
ENV NEXT_PUBLIC_SW_DEACTIVATED=${SW_DEACTIVATED}
|
||||
|
||||
ARG PUBLISH_AS_MIT
|
||||
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:1.27-alpine AS frontend-production
|
||||
|
||||
# Un-privileged user running the application
|
||||
ARG DOCKER_USER
|
||||
|
||||
@@ -7,7 +7,6 @@ export const CONFIG = {
|
||||
ENVIRONMENT: 'development',
|
||||
FRONTEND_CSS_URL: null,
|
||||
FRONTEND_HOMEPAGE_FEATURE_ENABLED: true,
|
||||
FRONTEND_FOOTER_FEATURE_ENABLED: true,
|
||||
FRONTEND_THEME: 'default',
|
||||
MEDIA_BASE_URL: 'http://localhost:8083',
|
||||
LANGUAGES: [
|
||||
@@ -20,6 +19,7 @@ export const CONFIG = {
|
||||
LANGUAGE_CODE: 'en-us',
|
||||
POSTHOG_KEY: {},
|
||||
SENTRY_DSN: null,
|
||||
theme_customization: {},
|
||||
};
|
||||
|
||||
export const keyCloakSignIn = async (
|
||||
|
||||
@@ -452,4 +452,41 @@ test.describe('Doc Editor', () => {
|
||||
const svgBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
expect(svgBuffer.toString()).toContain('Hello svg');
|
||||
});
|
||||
|
||||
test('it checks if callout custom block', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'doc-toolbar', browserName, 1);
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
await editor.click();
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Add a callout block').click();
|
||||
|
||||
const calloutBlock = page
|
||||
.locator('div[data-content-type="callout"]')
|
||||
.first();
|
||||
|
||||
await expect(calloutBlock).toBeVisible();
|
||||
|
||||
await calloutBlock.locator('.inline-content').fill('example text');
|
||||
|
||||
await expect(page.locator('.bn-block').first()).toHaveAttribute(
|
||||
'data-background-color',
|
||||
'yellow',
|
||||
);
|
||||
|
||||
const emojiButton = calloutBlock.getByRole('button');
|
||||
await expect(emojiButton).toHaveText('💡');
|
||||
await emojiButton.click();
|
||||
await page.locator('button[aria-label="⚠️"]').click();
|
||||
await expect(emojiButton).toHaveText('⚠️');
|
||||
|
||||
await page.locator('.bn-side-menu > button').last().click();
|
||||
await page.locator('.mantine-Menu-dropdown > button').last().click();
|
||||
await page.locator('.bn-color-picker-dropdown > button').last().click();
|
||||
|
||||
await expect(page.locator('.bn-block').first()).toHaveAttribute(
|
||||
'data-background-color',
|
||||
'pink',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -141,7 +141,7 @@ test.describe('Doc Export', () => {
|
||||
|
||||
/**
|
||||
* This test tell us that the export to pdf is working with images
|
||||
* but it does not tell us if the images are beeing displayed correctly
|
||||
* but it does not tell us if the images are being displayed correctly
|
||||
* in the pdf.
|
||||
*
|
||||
* TODO: Check if the images are displayed correctly in the pdf
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "3.2.0",
|
||||
"version": "3.2.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=
|
||||
NEXT_PUBLIC_SW_DEACTIVATED=
|
||||
NEXT_PUBLIC_PUBLISH_AS_MIT=true
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
|
||||
NEXT_PUBLIC_PUBLISH_AS_MIT=false
|
||||
NEXT_PUBLIC_SW_DEACTIVATED=true
|
||||
|
||||
@@ -48,10 +48,7 @@ const nextConfig = {
|
||||
swDest: '../public/service-worker.js',
|
||||
include: [
|
||||
({ asset }) => {
|
||||
if (asset.name.match(/.*(static).*/)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return !!asset.name.match(/.*(static).*/);
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "3.2.0",
|
||||
"version": "3.2.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -22,26 +22,29 @@
|
||||
"@blocknote/react": "0.29.1",
|
||||
"@blocknote/xl-docx-exporter": "0.29.1",
|
||||
"@blocknote/xl-pdf-exporter": "0.29.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",
|
||||
"@hocuspocus/provider": "2.15.2",
|
||||
"@openfun/cunningham-react": "3.0.0",
|
||||
"@react-pdf/renderer": "4.3.0",
|
||||
"@sentry/nextjs": "9.14.0",
|
||||
"@tanstack/react-query": "5.74.9",
|
||||
"@sentry/nextjs": "9.15.0",
|
||||
"@tanstack/react-query": "5.75.4",
|
||||
"canvg": "4.0.3",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"docx": "9.4.1",
|
||||
"i18next": "25.0.2",
|
||||
"i18next-browser-languagedetector": "8.0.5",
|
||||
"emoji-mart": "5.6.0",
|
||||
"i18next": "25.1.1",
|
||||
"i18next-browser-languagedetector": "8.1.0",
|
||||
"idb": "8.0.2",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.6.1",
|
||||
"next": "15.3.1",
|
||||
"posthog-js": "1.236.8",
|
||||
"posthog-js": "1.239.1",
|
||||
"react": "*",
|
||||
"react-aria-components": "1.8.0",
|
||||
"react-dom": "*",
|
||||
@@ -52,11 +55,11 @@
|
||||
"use-debounce": "10.0.4",
|
||||
"y-protocols": "1.0.6",
|
||||
"yjs": "*",
|
||||
"zustand": "5.0.3"
|
||||
"zustand": "5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@tanstack/react-query-devtools": "5.74.9",
|
||||
"@tanstack/react-query-devtools": "5.75.4",
|
||||
"@testing-library/dom": "10.4.0",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "16.3.0",
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
{
|
||||
"default": {
|
||||
"externalLinks": [
|
||||
{
|
||||
"label": "legifrance.gouv.fr",
|
||||
"href": "https://legifrance.gouv.fr/"
|
||||
},
|
||||
{
|
||||
"label": "info.gouv.fr",
|
||||
"href": "https://info.gouv.fr/"
|
||||
},
|
||||
{
|
||||
"label": "service-public.fr",
|
||||
"href": "https://service-public.fr/"
|
||||
},
|
||||
{
|
||||
"label": "data.gouv.fr",
|
||||
"href": "https://data.gouv.fr/"
|
||||
}
|
||||
],
|
||||
"legalLinks": [
|
||||
{
|
||||
"label": "Legal Notice",
|
||||
"href": "https://docs.numerique.gouv.fr/docs/4db744e3-5b47-4ed9-b9e7-d4312318bbce/"
|
||||
},
|
||||
{
|
||||
"label": "Personal data and cookies",
|
||||
"href": "https://docs.numerique.gouv.fr/docs/eb863389-a5e5-4d18-879d-149a1122380e/"
|
||||
},
|
||||
{
|
||||
"label": "Accessibility",
|
||||
"href": "https://docs.numerique.gouv.fr/docs/9694e570-1427-4ef7-b0a0-c3e894360e1b/"
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"legalLinks": [
|
||||
{
|
||||
"label": "Legal Notice",
|
||||
"href": "https://docs.numerique.gouv.fr/docs/4db744e3-5b47-4ed9-b9e7-d4312318bbce/"
|
||||
},
|
||||
{
|
||||
"label": "Personal data and cookies",
|
||||
"href": "https://docs.numerique.gouv.fr/docs/eb863389-a5e5-4d18-879d-149a1122380e/"
|
||||
},
|
||||
{
|
||||
"label": "Accessibility",
|
||||
"href": "https://docs.numerique.gouv.fr/docs/9694e570-1427-4ef7-b0a0-c3e894360e1b/"
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"legalLinks": [
|
||||
{
|
||||
"label": "Mentions légales",
|
||||
"href": "https://docs.numerique.gouv.fr/docs/4db744e3-5b47-4ed9-b9e7-d4312318bbce/"
|
||||
},
|
||||
{
|
||||
"label": "Données personnelles et cookies",
|
||||
"href": "https://docs.numerique.gouv.fr/docs/eb863389-a5e5-4d18-879d-149a1122380e/"
|
||||
},
|
||||
{
|
||||
"label": "Accessibilité",
|
||||
"href": "https://docs.numerique.gouv.fr/docs/9694e570-1427-4ef7-b0a0-c3e894360e1b/"
|
||||
}
|
||||
],
|
||||
"bottomInformation": {
|
||||
"label": "Sauf mention contraire, tout le contenu de ce site est sous",
|
||||
"link": {
|
||||
"label": "licence etalab-2.0",
|
||||
"href": "https://github.com/etalab/licence-ouverte/blob/master/LO.md"
|
||||
}
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"legalLinks": [
|
||||
{
|
||||
"label": "Impressum",
|
||||
"href": "https://docs.numerique.gouv.fr/docs/4db744e3-5b47-4ed9-b9e7-d4312318bbce/"
|
||||
},
|
||||
{
|
||||
"label": "Personenbezogene Daten und Cookies",
|
||||
"href": "https://docs.numerique.gouv.fr/docs/eb863389-a5e5-4d18-879d-149a1122380e/"
|
||||
},
|
||||
{
|
||||
"label": "Barrierefreiheit",
|
||||
"href": "https://docs.numerique.gouv.fr/docs/9694e570-1427-4ef7-b0a0-c3e894360e1b/"
|
||||
}
|
||||
],
|
||||
"bottomInformation": {
|
||||
"label": "Sofern nicht anders angegeben, steht der gesamte Inhalt dieser Website unter",
|
||||
"link": {
|
||||
"label": "licence etalab-2.0",
|
||||
"href": "https://github.com/etalab/licence-ouverte/blob/master/LO.md"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"legalLinks": [
|
||||
{
|
||||
"label": "Wettelijke bepalingen",
|
||||
"href": "https://docs.numerique.gouv.fr/docs/4db744e3-5b47-4ed9-b9e7-d4312318bbce/"
|
||||
},
|
||||
{
|
||||
"label": "Persoonlijke gegevens en cookies",
|
||||
"href": "https://docs.numerique.gouv.fr/docs/eb863389-a5e5-4d18-879d-149a1122380e/"
|
||||
},
|
||||
{
|
||||
"label": "Toegankelijkheid",
|
||||
"href": "https://docs.numerique.gouv.fr/docs/9694e570-1427-4ef7-b0a0-c3e894360e1b/"
|
||||
}
|
||||
],
|
||||
"bottomInformation": {
|
||||
"label": "Tenzij anders vermeld, is alle inhoud van deze site ondergebracht onder",
|
||||
"link": {
|
||||
"label": "licence etalab-2.0",
|
||||
"href": "https://github.com/etalab/licence-ouverte/blob/master/LO.md"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,39 @@
|
||||
/**
|
||||
* Generic interface for representing an API error structure.
|
||||
*
|
||||
* @template T - Optional type of additional data returned with the error.
|
||||
*/
|
||||
interface IAPIError<T = unknown> {
|
||||
/** HTTP status code or API-defined error code */
|
||||
status: number;
|
||||
/** Optional list of error causes (e.g., validation issues) */
|
||||
cause?: string[];
|
||||
/** Optional extra data provided with the error */
|
||||
data?: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom error class for representing API errors.
|
||||
* Extends the native Error object with additional context such as HTTP status,
|
||||
* causes, and extra data returned by the API.
|
||||
*
|
||||
* @template T - Optional type of the `data` field
|
||||
*/
|
||||
export class APIError<T = unknown> extends Error implements IAPIError<T> {
|
||||
public status: IAPIError['status'];
|
||||
public cause?: IAPIError['cause'];
|
||||
public data?: IAPIError<T>['data'];
|
||||
|
||||
/**
|
||||
* Constructs a new APIError instance.
|
||||
*
|
||||
* @param message - The human-readable error message.
|
||||
* @param status - The HTTP status code or equivalent.
|
||||
* @param cause - (Optional) List of strings describing error causes.
|
||||
* @param data - (Optional) Any additional data returned by the API.
|
||||
*/
|
||||
constructor(message: string, { status, cause, data }: IAPIError<T>) {
|
||||
super(message);
|
||||
|
||||
this.name = 'APIError';
|
||||
this.status = status;
|
||||
this.cause = cause;
|
||||
@@ -19,6 +41,12 @@ export class APIError<T = unknown> extends Error implements IAPIError<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for checking if a value is an instance of APIError.
|
||||
*
|
||||
* @param error - The value to check.
|
||||
* @returns True if the value is an instance of APIError.
|
||||
*/
|
||||
export const isAPIError = (error: unknown): error is APIError => {
|
||||
return error instanceof APIError;
|
||||
};
|
||||
|
||||
36
src/frontend/apps/impress/src/api/__tests__/APIError.test.ts
Normal file
36
src/frontend/apps/impress/src/api/__tests__/APIError.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { APIError, isAPIError } from '@/api';
|
||||
|
||||
describe('APIError', () => {
|
||||
it('should correctly instantiate with required fields', () => {
|
||||
const error = new APIError('Something went wrong', { status: 500 });
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error).toBeInstanceOf(APIError);
|
||||
expect(error.message).toBe('Something went wrong');
|
||||
expect(error.status).toBe(500);
|
||||
expect(error.cause).toBeUndefined();
|
||||
expect(error.data).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should correctly instantiate with all fields', () => {
|
||||
const details = { field: 'email' };
|
||||
const error = new APIError('Validation failed', {
|
||||
status: 400,
|
||||
cause: ['Invalid email format'],
|
||||
data: details,
|
||||
});
|
||||
|
||||
expect(error.name).toBe('APIError');
|
||||
expect(error.status).toBe(400);
|
||||
expect(error.cause).toEqual(['Invalid email format']);
|
||||
expect(error.data).toEqual(details);
|
||||
});
|
||||
|
||||
it('should be detected by isAPIError type guard', () => {
|
||||
const error = new APIError('Unauthorized', { status: 401 });
|
||||
const notAnError = { message: 'Fake error' };
|
||||
|
||||
expect(isAPIError(error)).toBe(true);
|
||||
expect(isAPIError(notAnError)).toBe(false);
|
||||
});
|
||||
});
|
||||
16
src/frontend/apps/impress/src/api/__tests__/config.test.ts
Normal file
16
src/frontend/apps/impress/src/api/__tests__/config.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { baseApiUrl } from '@/api';
|
||||
|
||||
describe('config', () => {
|
||||
it('constructs URL with default version', () => {
|
||||
expect(baseApiUrl()).toBe('http://test.jest/api/v1.0/');
|
||||
});
|
||||
|
||||
it('constructs URL with custom version', () => {
|
||||
expect(baseApiUrl('2.0')).toBe('http://test.jest/api/v2.0/');
|
||||
});
|
||||
|
||||
it('uses env origin if available', () => {
|
||||
process.env.NEXT_PUBLIC_API_ORIGIN = 'https://env.example.com';
|
||||
expect(baseApiUrl('3.0')).toBe('https://env.example.com/api/v3.0/');
|
||||
});
|
||||
});
|
||||
@@ -36,4 +36,13 @@ describe('fetchAPI', () => {
|
||||
|
||||
expect(fetchMock.lastUrl()).toEqual('http://test.jest/api/v2.0/some/url');
|
||||
});
|
||||
|
||||
it('removes Content-Type header when withoutContentType is true', async () => {
|
||||
fetchMock.mock('http://test.jest/api/v1.0/some/url', 200);
|
||||
|
||||
await fetchAPI('some/url', { withoutContentType: true });
|
||||
|
||||
const options = fetchMock.lastOptions();
|
||||
expect(options?.headers).not.toHaveProperty('Content-Type');
|
||||
});
|
||||
});
|
||||
|
||||
59
src/frontend/apps/impress/src/api/__tests__/helpers.test.tsx
Normal file
59
src/frontend/apps/impress/src/api/__tests__/helpers.test.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
|
||||
import { useAPIInfiniteQuery } from '@/api';
|
||||
|
||||
interface DummyItem {
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface DummyResponse {
|
||||
results: DummyItem[];
|
||||
next?: string;
|
||||
}
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient();
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('helpers', () => {
|
||||
it('fetches and paginates correctly', async () => {
|
||||
const mockAPI = jest
|
||||
.fn<Promise<DummyResponse>, [{ page: number; query: string }]>()
|
||||
.mockResolvedValueOnce({
|
||||
results: [{ id: 1 }],
|
||||
next: 'url?page=2',
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
results: [{ id: 2 }],
|
||||
next: undefined,
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useAPIInfiniteQuery('test-key', mockAPI, { query: 'test' }),
|
||||
{ wrapper: createWrapper() },
|
||||
);
|
||||
|
||||
// Wait for first page
|
||||
await waitFor(() => {
|
||||
expect(result.current.data?.pages[0].results[0].id).toBe(1);
|
||||
});
|
||||
|
||||
// Fetch next page
|
||||
await result.current.fetchNextPage();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.data?.pages.length).toBe(2);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.data?.pages[1].results[0].id).toBe(2);
|
||||
});
|
||||
|
||||
expect(mockAPI).toHaveBeenCalledWith({ query: 'test', page: 1 });
|
||||
expect(mockAPI).toHaveBeenCalledWith({ query: 'test', page: 2 });
|
||||
});
|
||||
});
|
||||
57
src/frontend/apps/impress/src/api/__tests__/utils.test.ts
Normal file
57
src/frontend/apps/impress/src/api/__tests__/utils.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { errorCauses, getCSRFToken } from '@/api';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('errorCauses', () => {
|
||||
const createMockResponse = (jsonData: any, status = 400): Response => {
|
||||
return {
|
||||
status,
|
||||
json: () => jsonData,
|
||||
} as unknown as Response;
|
||||
};
|
||||
|
||||
it('parses multiple string causes from error body', async () => {
|
||||
const mockResponse = createMockResponse(
|
||||
{
|
||||
field: ['error message 1', 'error message 2'],
|
||||
},
|
||||
400,
|
||||
);
|
||||
|
||||
const result = await errorCauses(mockResponse, { context: 'login' });
|
||||
|
||||
expect(result.status).toBe(400);
|
||||
expect(result.cause).toEqual(['error message 1', 'error message 2']);
|
||||
expect(result.data).toEqual({ context: 'login' });
|
||||
});
|
||||
|
||||
it('returns undefined causes if no JSON body', async () => {
|
||||
const mockResponse = createMockResponse(null, 500);
|
||||
|
||||
const result = await errorCauses(mockResponse);
|
||||
|
||||
expect(result.status).toBe(500);
|
||||
expect(result.cause).toBeUndefined();
|
||||
expect(result.data).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCSRFToken', () => {
|
||||
it('extracts csrftoken from document.cookie', () => {
|
||||
Object.defineProperty(document, 'cookie', {
|
||||
writable: true,
|
||||
value: 'sessionid=xyz; csrftoken=abc123; theme=dark',
|
||||
});
|
||||
|
||||
expect(getCSRFToken()).toBe('abc123');
|
||||
});
|
||||
|
||||
it('returns undefined if csrftoken is not present', () => {
|
||||
Object.defineProperty(document, 'cookie', {
|
||||
writable: true,
|
||||
value: 'sessionid=xyz; theme=dark',
|
||||
});
|
||||
|
||||
expect(getCSRFToken()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,22 @@
|
||||
/**
|
||||
* Returns the base URL for the backend API.
|
||||
*
|
||||
* Priority:
|
||||
* 1. Uses NEXT_PUBLIC_API_ORIGIN from environment variables if defined.
|
||||
* 2. Falls back to the browser's window.location.origin if in a browser environment.
|
||||
* 3. Defaults to an empty string if executed in a non-browser environment without the env variable.
|
||||
*
|
||||
* @returns The backend base URL as a string.
|
||||
*/
|
||||
export const backendUrl = () =>
|
||||
process.env.NEXT_PUBLIC_API_ORIGIN ||
|
||||
(typeof window !== 'undefined' ? window.location.origin : '');
|
||||
|
||||
/**
|
||||
* Constructs the full base API URL, including the versioned path (e.g., `/api/v1.0/`).
|
||||
*
|
||||
* @param apiVersion - The version of the API (defaults to '1.0').
|
||||
* @returns The full versioned API base URL as a string.
|
||||
*/
|
||||
export const baseApiUrl = (apiVersion: string = '1.0') =>
|
||||
`${backendUrl()}/api/v${apiVersion}/`;
|
||||
|
||||
@@ -23,11 +23,9 @@ export const fetchAPI = async (
|
||||
delete headers?.['Content-Type' as keyof typeof headers];
|
||||
}
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
return await fetch(apiUrl, {
|
||||
...init,
|
||||
credentials: 'include',
|
||||
headers,
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
@@ -22,9 +22,17 @@ export type DefinedInitialDataInfiniteOptionsAPI<
|
||||
>;
|
||||
|
||||
/**
|
||||
* @param param Used for infinite scroll pagination
|
||||
* @param queryConfig
|
||||
* @returns
|
||||
* Custom React hook that wraps React Query's `useInfiniteQuery` for paginated API requests.
|
||||
*
|
||||
* @template T - Type of the request parameters.
|
||||
* @template Q - Type of the API response, which must include an optional `next` field for pagination.
|
||||
*
|
||||
* @param {string} key - Unique key to identify the query in the cache.
|
||||
* @param {(props: T & { page: number }) => Promise<Q>} api - Function that fetches paginated data from the API. It receives the params merged with a page number.
|
||||
* @param {T} param - Static parameters to send with every API request (excluding the page number).
|
||||
* @param {DefinedInitialDataInfiniteOptionsAPI<Q>} [queryConfig] - Optional configuration passed to `useInfiniteQuery` (e.g., stale time, cache time).
|
||||
*
|
||||
* @returns Return value of `useInfiniteQuery`, including data, loading state, fetchNextPage, etc.
|
||||
*/
|
||||
export const useAPIInfiniteQuery = <T, Q extends { next?: APIList<Q>['next'] }>(
|
||||
key: string,
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
/**
|
||||
* Generic interface representing a paginated API response.
|
||||
*
|
||||
* Commonly used for endpoints that return list results with pagination metadata.
|
||||
*
|
||||
* @template T - The type of items in the `results` array.
|
||||
*/
|
||||
export interface APIList<T> {
|
||||
/** Total number of items across all pages */
|
||||
count: number;
|
||||
|
||||
/** URL to the next page of results, if available (can be null or undefined) */
|
||||
next?: string | null;
|
||||
|
||||
/** URL to the previous page of results, if available (can be null or undefined) */
|
||||
previous?: string | null;
|
||||
|
||||
/** The list of items for the current page */
|
||||
results: T[];
|
||||
}
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
/**
|
||||
* Extracts error information from an HTTP `Response` object.
|
||||
*
|
||||
* This is typically used to parse structured error responses from an API
|
||||
* and normalize them into a consistent format with `status`, `cause`, and optional `data`.
|
||||
*
|
||||
* @param response - The HTTP response object from `fetch()`.
|
||||
* @param data - Optional custom data to include with the error output.
|
||||
* @returns An object containing:
|
||||
* - `status`: HTTP status code from the response
|
||||
* - `cause`: A flattened list of error messages, or undefined if no body
|
||||
* - `data`: The optional data passed in
|
||||
*/
|
||||
export const errorCauses = async (response: Response, data?: unknown) => {
|
||||
const errorsBody = (await response.json()) as Record<
|
||||
string,
|
||||
@@ -18,7 +31,11 @@ export const errorCauses = async (response: Response, data?: unknown) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the CSRF token from the document's cookies.
|
||||
* Retrieves the CSRF token from the browser's cookies.
|
||||
*
|
||||
* Assumes the CSRF token is stored as a cookie named "csrftoken".
|
||||
*
|
||||
* @returns The CSRF token string if found, otherwise `undefined`.
|
||||
*/
|
||||
export function getCSRFToken() {
|
||||
return document.cookie
|
||||
|
||||
@@ -10,7 +10,7 @@ type TextSizes = keyof typeof sizes;
|
||||
|
||||
export interface TextProps extends BoxProps {
|
||||
as?: 'p' | 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||
$elipsis?: boolean;
|
||||
$ellipsis?: boolean;
|
||||
$weight?: CSSProperties['fontWeight'];
|
||||
$textAlign?: CSSProperties['textAlign'];
|
||||
$size?: TextSizes | (string & {});
|
||||
@@ -50,8 +50,8 @@ export const TextStyled = styled(Box)<TextProps>`
|
||||
${({ $theme, $variation }) =>
|
||||
`color: var(--c--theme--colors--${$theme}-${$variation});`}
|
||||
${({ $color }) => $color && `color: ${$color};`}
|
||||
${({ $elipsis }) =>
|
||||
$elipsis &&
|
||||
${({ $ellipsis }) =>
|
||||
$ellipsis &&
|
||||
`white-space: nowrap; overflow: hidden; text-overflow: ellipsis;`}
|
||||
`;
|
||||
|
||||
|
||||
@@ -35,8 +35,7 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const cleanupResizeListener = initializeResizeListener();
|
||||
return cleanupResizeListener;
|
||||
return initializeResizeListener();
|
||||
}, [initializeResizeListener]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -53,13 +53,11 @@ export function useConfig() {
|
||||
const cachedData = getCachedTranslation();
|
||||
const oneHour = 1000 * 60 * 60;
|
||||
|
||||
const response = useQuery<ConfigResponse, APIError, ConfigResponse>({
|
||||
return useQuery<ConfigResponse, APIError, ConfigResponse>({
|
||||
queryKey: [KEY_CONFIG],
|
||||
queryFn: () => getConfig(),
|
||||
initialData: cachedData,
|
||||
staleTime: oneHour,
|
||||
initialDataUpdatedAt: Date.now() - oneHour, // Force initial data to be considered stale
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ declare module '*.svg?url' {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NEXT_PUBLIC_API_ORIGIN?: string;
|
||||
NEXT_PUBLIC_PUBLISH_AS_MIT?: string;
|
||||
NEXT_PUBLIC_SW_DEACTIVATED?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,13 @@ import { randomColor } from '../utils';
|
||||
|
||||
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
|
||||
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
|
||||
import { DividerBlock } from './custom-blocks';
|
||||
import { CalloutBlock, DividerBlock } from './custom-blocks';
|
||||
|
||||
export const blockNoteSchema = withPageBreak(
|
||||
BlockNoteSchema.create({
|
||||
blockSpecs: {
|
||||
...defaultBlockSpecs,
|
||||
callout: CalloutBlock,
|
||||
divider: DividerBlock,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -11,7 +11,10 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { DocsBlockSchema } from '../types';
|
||||
|
||||
import { getDividerReactSlashMenuItems } from './custom-blocks';
|
||||
import {
|
||||
getCalloutReactSlashMenuItems,
|
||||
getDividerReactSlashMenuItems,
|
||||
} from './custom-blocks';
|
||||
|
||||
export const BlockNoteSuggestionMenu = () => {
|
||||
const editor = useBlockNoteEditor<DocsBlockSchema>();
|
||||
@@ -25,6 +28,7 @@ export const BlockNoteSuggestionMenu = () => {
|
||||
combineByGroup(
|
||||
getDefaultReactSlashMenuItems(editor),
|
||||
getPageBreakReactSlashMenuItems(editor),
|
||||
getCalloutReactSlashMenuItems(editor, t, basicBlocksName),
|
||||
getDividerReactSlashMenuItems(editor, t, basicBlocksName),
|
||||
),
|
||||
query,
|
||||
|
||||
@@ -6,9 +6,12 @@ import {
|
||||
useDictionary,
|
||||
} from '@blocknote/react';
|
||||
import React, { JSX, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useConfig } from '@/core/config/api';
|
||||
|
||||
import { getCalloutFormattingToolbarItems } from '../custom-blocks';
|
||||
|
||||
import { AIGroupButton } from './AIButton';
|
||||
import { FileDownloadButton } from './FileDownloadButton';
|
||||
import { MarkdownButton } from './MarkdownButton';
|
||||
@@ -18,11 +21,13 @@ export const BlockNoteToolbar = () => {
|
||||
const dict = useDictionary();
|
||||
const [confirmOpen, setIsConfirmOpen] = useState(false);
|
||||
const [onConfirm, setOnConfirm] = useState<() => void | Promise<void>>();
|
||||
const { t } = useTranslation();
|
||||
const { data: conf } = useConfig();
|
||||
|
||||
const toolbarItems = useMemo(() => {
|
||||
const toolbarItems = getFormattingToolbarItems([
|
||||
...blockTypeSelectItems(dict),
|
||||
getCalloutFormattingToolbarItems(t),
|
||||
]);
|
||||
const fileDownloadButtonIndex = toolbarItems.findIndex(
|
||||
(item) =>
|
||||
@@ -46,7 +51,7 @@ export const BlockNoteToolbar = () => {
|
||||
}
|
||||
|
||||
return toolbarItems as JSX.Element[];
|
||||
}, [dict]);
|
||||
}, [dict, t]);
|
||||
|
||||
const formattingToolbar = useCallback(() => {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import data from '@emoji-mart/data';
|
||||
import Picker from '@emoji-mart/react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box } from '@/components';
|
||||
|
||||
interface EmojiPickerProps {
|
||||
categories: string[];
|
||||
custom: {
|
||||
name: string;
|
||||
id: string;
|
||||
emojis: string[];
|
||||
}[];
|
||||
onClickOutside: () => void;
|
||||
onEmojiSelect: ({ native }: { native: string }) => void;
|
||||
}
|
||||
|
||||
export const EmojiPicker = ({
|
||||
categories,
|
||||
custom,
|
||||
onClickOutside,
|
||||
onEmojiSelect,
|
||||
}: EmojiPickerProps) => {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box $position="absolute" $zIndex={1000} $margin="2rem 0 0 0">
|
||||
<Picker
|
||||
categories={categories}
|
||||
custom={custom}
|
||||
data={data}
|
||||
locale={i18n.resolvedLanguage}
|
||||
navPosition="none"
|
||||
onClickOutside={onClickOutside}
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
previewPosition="none"
|
||||
skinTonePosition="none"
|
||||
theme="light"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { defaultProps, insertOrUpdateBlock } from '@blocknote/core';
|
||||
import { BlockTypeSelectItem, createReactBlockSpec } from '@blocknote/react';
|
||||
import { TFunction } from 'i18next';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, Icon } from '@/components';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../../types';
|
||||
import { EmojiPicker } from '../EmojiPicker';
|
||||
|
||||
const calloutCustom = [
|
||||
{
|
||||
name: 'Callout',
|
||||
id: 'callout',
|
||||
emojis: [
|
||||
'bulb',
|
||||
'point_right',
|
||||
'point_up',
|
||||
'ok_hand',
|
||||
'key',
|
||||
'construction',
|
||||
'warning',
|
||||
'fire',
|
||||
'pushpin',
|
||||
'scissors',
|
||||
'question',
|
||||
'no_entry',
|
||||
'no_entry_sign',
|
||||
'alarm_clock',
|
||||
'phone',
|
||||
'rotating_light',
|
||||
'recycle',
|
||||
'white_check_mark',
|
||||
'lock',
|
||||
'paperclip',
|
||||
'book',
|
||||
'speaking_head_in_silhouette',
|
||||
'arrow_right',
|
||||
'loudspeaker',
|
||||
'hammer_and_wrench',
|
||||
'gear',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const calloutCategories = [
|
||||
'callout',
|
||||
'people',
|
||||
'nature',
|
||||
'foods',
|
||||
'activity',
|
||||
'places',
|
||||
'flags',
|
||||
'objects',
|
||||
'symbols',
|
||||
];
|
||||
|
||||
export const CalloutBlock = createReactBlockSpec(
|
||||
{
|
||||
type: 'callout',
|
||||
propSchema: {
|
||||
textAlignment: defaultProps.textAlignment,
|
||||
backgroundColor: defaultProps.backgroundColor,
|
||||
emoji: { default: '💡' },
|
||||
},
|
||||
content: 'inline',
|
||||
},
|
||||
{
|
||||
render: ({ block, editor, contentRef }) => {
|
||||
const [openEmojiPicker, setOpenEmojiPicker] = useState(false);
|
||||
|
||||
const toggleEmojiPicker = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setOpenEmojiPicker(!openEmojiPicker);
|
||||
};
|
||||
|
||||
const onClickOutside = () => setOpenEmojiPicker(false);
|
||||
|
||||
const onEmojiSelect = ({ native }: { native: string }) => {
|
||||
editor.updateBlock(block, { props: { emoji: native } });
|
||||
setOpenEmojiPicker(false);
|
||||
};
|
||||
|
||||
// Temporary: sets a yellow background color to a callout block when added by
|
||||
// the user, while keeping the colors menu on the drag handler usable for
|
||||
// this custom block.
|
||||
useEffect(() => {
|
||||
if (
|
||||
!block.content.length &&
|
||||
block.props.backgroundColor === 'default'
|
||||
) {
|
||||
editor.updateBlock(block, { props: { backgroundColor: 'yellow' } });
|
||||
}
|
||||
}, [block, editor]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
$padding="1rem"
|
||||
$gap="0.625rem"
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
flexDirection: 'row',
|
||||
}}
|
||||
>
|
||||
<BoxButton
|
||||
contentEditable={false}
|
||||
onClick={toggleEmojiPicker}
|
||||
$css={css`
|
||||
font-size: 1.125rem;
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`}
|
||||
$align="center"
|
||||
$height="28px"
|
||||
$width="28px"
|
||||
$radius="4px"
|
||||
>
|
||||
{block.props.emoji}
|
||||
</BoxButton>
|
||||
|
||||
{openEmojiPicker && (
|
||||
<EmojiPicker
|
||||
categories={calloutCategories}
|
||||
custom={calloutCustom}
|
||||
onClickOutside={onClickOutside}
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
/>
|
||||
)}
|
||||
<Box as="p" className="inline-content" ref={contentRef} />
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const getCalloutReactSlashMenuItems = (
|
||||
editor: DocsBlockNoteEditor,
|
||||
t: TFunction<'translation', undefined>,
|
||||
group: string,
|
||||
) => [
|
||||
{
|
||||
title: t('Callout'),
|
||||
onItemClick: () => {
|
||||
insertOrUpdateBlock(editor, {
|
||||
type: 'callout',
|
||||
});
|
||||
},
|
||||
aliases: ['callout', 'encadré', 'hervorhebung', 'benadrukken'],
|
||||
group,
|
||||
icon: <Icon iconName="lightbulb" $size="18px" />,
|
||||
subtext: t('Add a callout block'),
|
||||
},
|
||||
];
|
||||
|
||||
export const getCalloutFormattingToolbarItems = (
|
||||
t: TFunction<'translation', undefined>,
|
||||
): BlockTypeSelectItem => ({
|
||||
name: t('Callout'),
|
||||
type: 'callout',
|
||||
icon: () => <Icon iconName="lightbulb" $size="16px" />,
|
||||
isSelected: (block) => block.type === 'callout',
|
||||
});
|
||||
@@ -1 +1,2 @@
|
||||
export * from './CalloutBlock';
|
||||
export * from './DividerBlock';
|
||||
|
||||
@@ -31,7 +31,7 @@ const useSaveDoc = (docId: string, yDoc: Y.Doc, canSave: boolean) => {
|
||||
_updatedDoc: Y.Doc,
|
||||
transaction: Y.Transaction,
|
||||
) => {
|
||||
setIsLocalChange(transaction.local ? true : false);
|
||||
setIsLocalChange(transaction.local);
|
||||
};
|
||||
|
||||
yDoc.on('update', onUpdate);
|
||||
@@ -61,7 +61,7 @@ const useSaveDoc = (docId: string, yDoc: Y.Doc, canSave: boolean) => {
|
||||
const isSaving = saveDoc();
|
||||
|
||||
/**
|
||||
* Firefox does not trigger the request everytime the user leaves the page.
|
||||
* Firefox does not trigger the request every time the user leaves the page.
|
||||
* Plus the request is not intercepted by the service worker.
|
||||
* So we prevent the default behavior to have the popup asking the user
|
||||
* if he wants to leave the page, by adding the popup, we let the time to the
|
||||
|
||||
@@ -61,6 +61,23 @@ export const cssEditor = (readonly: boolean) => css`
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callout, Paragraph and Heading blocks
|
||||
*/
|
||||
.bn-block {
|
||||
border-radius: var(--c--theme--spacings--3xs);
|
||||
}
|
||||
|
||||
.bn-block-outer {
|
||||
border-radius: var(--c--theme--spacings--3xs);
|
||||
}
|
||||
|
||||
.bn-block-content[data-content-type='paragraph'],
|
||||
.bn-block-content[data-content-type='heading'] {
|
||||
padding: var(--c--theme--spacings--3xs) var(--c--theme--spacings--3xs);
|
||||
border-radius: var(--c--theme--spacings--3xs);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Paragraph, TextRun } from 'docx';
|
||||
|
||||
import { DocsExporterDocx } from '../types';
|
||||
import { docxBlockPropsToStyles } from '../utils';
|
||||
|
||||
export const blockMappingCalloutDocx: DocsExporterDocx['mappings']['blockMapping']['callout'] =
|
||||
(block, exporter) => {
|
||||
return new Paragraph({
|
||||
...docxBlockPropsToStyles(block.props, exporter.options.colors),
|
||||
spacing: { before: 10, after: 10 },
|
||||
children: [
|
||||
new TextRun({
|
||||
text: ' ',
|
||||
break: 1,
|
||||
}),
|
||||
new TextRun(' ' + block.props.emoji + ' '),
|
||||
...exporter.transformInlineContent(block.content),
|
||||
new TextRun({
|
||||
text: ' ',
|
||||
break: 1,
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { StyleSheet, Text, View } from '@react-pdf/renderer';
|
||||
|
||||
import { DocsExporterPDF } from '../types';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
padding: 8,
|
||||
gap: 4,
|
||||
},
|
||||
emoji: {
|
||||
fontSize: 16,
|
||||
},
|
||||
text: {
|
||||
maxWidth: '94%',
|
||||
paddingTop: 2,
|
||||
},
|
||||
});
|
||||
|
||||
export const blockMappingCalloutPDF: DocsExporterPDF['mappings']['blockMapping']['callout'] =
|
||||
(block, exporter) => {
|
||||
return (
|
||||
<View wrap={false} style={styles.wrapper}>
|
||||
<Text debug={false} style={styles.emoji}>
|
||||
{block.props.emoji}
|
||||
</Text>
|
||||
<Text debug={false} style={styles.text}>
|
||||
{' '}
|
||||
{exporter.transformInlineContent(block.content)}{' '}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './calloutDocx';
|
||||
export * from './calloutPDF';
|
||||
export * from './dividerDocx';
|
||||
export * from './dividerPDF';
|
||||
export * from './headingPDF';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* See:
|
||||
* https://github.com/TypeCellOS/BlockNote/blob/004c0bf720fe1415c497ad56449015c5f4dd7ba0/packages/xl-pdf-exporter/src/pdf/util/table/Table.tsx
|
||||
*
|
||||
* We succeded to manage the colspan, but rowspan is not supported yet.
|
||||
* We succeeded to manage the colspan, but rowspan is not supported yet.
|
||||
*/
|
||||
|
||||
import { TD, TR, Table } from '@ag-media/react-pdf-table';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { docxDefaultSchemaMappings } from '@blocknote/xl-docx-exporter';
|
||||
|
||||
import {
|
||||
blockMappingCalloutDocx,
|
||||
blockMappingDividerDocx,
|
||||
blockMappingImageDocx,
|
||||
blockMappingQuoteDocx,
|
||||
@@ -11,6 +12,7 @@ export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
|
||||
...docxDefaultSchemaMappings,
|
||||
blockMapping: {
|
||||
...docxDefaultSchemaMappings.blockMapping,
|
||||
callout: blockMappingCalloutDocx,
|
||||
divider: blockMappingDividerDocx,
|
||||
quote: blockMappingQuoteDocx,
|
||||
image: blockMappingImageDocx,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { pdfDefaultSchemaMappings } from '@blocknote/xl-pdf-exporter';
|
||||
|
||||
import {
|
||||
blockMappingCalloutPDF,
|
||||
blockMappingDividerPDF,
|
||||
blockMappingHeadingPDF,
|
||||
blockMappingImagePDF,
|
||||
@@ -14,6 +15,7 @@ export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = {
|
||||
...pdfDefaultSchemaMappings,
|
||||
blockMapping: {
|
||||
...pdfDefaultSchemaMappings.blockMapping,
|
||||
callout: blockMappingCalloutPDF,
|
||||
heading: blockMappingHeadingPDF,
|
||||
image: blockMappingImagePDF,
|
||||
paragraph: blockMappingParagraphPDF,
|
||||
|
||||
@@ -19,9 +19,16 @@ export function downloadFile(blob: Blob, filename: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert SVG to PNG
|
||||
* @param svgText - The SVG text to convert
|
||||
* @returns The PNG data URL
|
||||
* Converts an SVG string into a PNG image and returns it as a data URL.
|
||||
*
|
||||
* This function creates a canvas, parses the SVG, calculates the appropriate height
|
||||
* to preserve the aspect ratio, and renders the SVG onto the canvas using Canvg.
|
||||
*
|
||||
* @param {string} svgText - The raw SVG markup to convert.
|
||||
* @param {number} width - The desired width of the output PNG (height is auto-calculated to preserve aspect ratio).
|
||||
* @returns {Promise<string>} A Promise that resolves to a PNG image encoded as a base64 data URL.
|
||||
*
|
||||
* @throws Will throw an error if the canvas context cannot be initialized.
|
||||
*/
|
||||
export async function convertSvgToPng(svgText: string, width: number) {
|
||||
// Create a canvas and render the SVG onto it
|
||||
|
||||
@@ -42,6 +42,7 @@ const doc = {
|
||||
|
||||
beforeEach(() => {
|
||||
Analytics.clearAnalytics();
|
||||
process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = 'false';
|
||||
});
|
||||
|
||||
describe('DocToolBox "Copy as HTML" option', () => {
|
||||
@@ -51,7 +52,9 @@ describe('DocToolBox "Copy as HTML" option', () => {
|
||||
render(<DocToolBox doc={doc as any} />, {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
const optionsButton = screen.getByLabelText('Open the document options');
|
||||
const optionsButton = await screen.findByLabelText(
|
||||
'Open the document options',
|
||||
);
|
||||
await userEvent.click(optionsButton);
|
||||
expect(await screen.findByText('Copy as HTML')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AppWrapper } from '@/tests/utils';
|
||||
|
||||
import { DocToolBox } from '../components/DocToolBox';
|
||||
|
||||
const doc = {
|
||||
nb_accesses: 1,
|
||||
abilities: {
|
||||
versions_list: true,
|
||||
destroy: true,
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('@/features/docs/doc-export/', () => ({
|
||||
ModalExport: () => <span>ModalExport</span>,
|
||||
}));
|
||||
|
||||
it('DocToolBox dynamic import: loads DocToolBox when NEXT_PUBLIC_PUBLISH_AS_MIT is false', async () => {
|
||||
process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = 'false';
|
||||
|
||||
render(<DocToolBox doc={doc as any} />, {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
|
||||
expect(await screen.findByText('download')).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AppWrapper } from '@/tests/utils';
|
||||
|
||||
const doc = {
|
||||
nb_accesses: 1,
|
||||
abilities: {
|
||||
versions_list: true,
|
||||
destroy: true,
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('@/features/docs/doc-export/', () => ({
|
||||
ModalExport: () => <span>ModalExport</span>,
|
||||
}));
|
||||
|
||||
it('DocToolBox dynamic import: loads DocToolBox when NEXT_PUBLIC_PUBLISH_AS_MIT is true', async () => {
|
||||
process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = 'true';
|
||||
|
||||
const { DocToolBox } = await import('../components/DocToolBox');
|
||||
|
||||
render(<DocToolBox doc={doc as any} />, {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.queryByText('download')).not.toBeInTheDocument();
|
||||
},
|
||||
{
|
||||
timeout: 1000,
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1,170 +1,47 @@
|
||||
import {
|
||||
Button,
|
||||
VariantType,
|
||||
useModal,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { Button, useModal } from '@openfun/cunningham-react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import {
|
||||
Box,
|
||||
DropdownMenu,
|
||||
DropdownMenuOption,
|
||||
Icon,
|
||||
IconOptions,
|
||||
} from '@/components';
|
||||
import { Box, Icon } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useEditorStore } from '@/docs/doc-editor/';
|
||||
import { ModalExport } from '@/docs/doc-export/';
|
||||
import {
|
||||
Doc,
|
||||
KEY_DOC,
|
||||
KEY_LIST_DOC,
|
||||
ModalRemoveDoc,
|
||||
useCopyDocLink,
|
||||
useCreateFavoriteDoc,
|
||||
useDeleteFavoriteDoc,
|
||||
} from '@/docs/doc-management';
|
||||
import { DocShareModal } from '@/docs/doc-share';
|
||||
import {
|
||||
KEY_LIST_DOC_VERSIONS,
|
||||
ModalSelectVersion,
|
||||
} from '@/docs/doc-versioning';
|
||||
import { useAnalytics } from '@/libs';
|
||||
import { Doc } from '@/docs/doc-management';
|
||||
import { KEY_LIST_DOC_VERSIONS } from '@/docs/doc-versioning';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
interface DocToolBoxProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
const DocToolBoxLicence = dynamic(() =>
|
||||
process.env.NEXT_PUBLIC_PUBLISH_AS_MIT === 'false'
|
||||
? import('./DocToolBoxLicenceAGPL').then((mod) => mod.DocToolBoxLicenceAGPL)
|
||||
: import('./DocToolBoxLicenceMIT').then((mod) => mod.DocToolBoxLicenceMIT),
|
||||
);
|
||||
|
||||
export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
const { t } = useTranslation();
|
||||
const hasAccesses = doc.nb_accesses_direct > 1 && doc.abilities.accesses_view;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
|
||||
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
|
||||
const [isModalExportOpen, setIsModalExportOpen] = useState(false);
|
||||
const selectHistoryModal = useModal();
|
||||
const modalHistory = useModal();
|
||||
const modalShare = useModal();
|
||||
|
||||
const { isSmallMobile, isDesktop } = useResponsiveStore();
|
||||
const { editor } = useEditorStore();
|
||||
const { toast } = useToastProvider();
|
||||
const copyDocLink = useCopyDocLink(doc.id);
|
||||
const { isFeatureFlagActivated } = useAnalytics();
|
||||
const removeFavoriteDoc = useDeleteFavoriteDoc({
|
||||
listInvalideQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||
});
|
||||
const makeFavoriteDoc = useCreateFavoriteDoc({
|
||||
listInvalideQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||
});
|
||||
|
||||
const options: DropdownMenuOption[] = [
|
||||
...(isSmallMobile
|
||||
? [
|
||||
{
|
||||
label: t('Share'),
|
||||
icon: 'group',
|
||||
callback: modalShare.open,
|
||||
},
|
||||
{
|
||||
label: t('Export'),
|
||||
icon: 'download',
|
||||
callback: () => {
|
||||
setIsModalExportOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('Copy link'),
|
||||
icon: 'add_link',
|
||||
callback: copyDocLink,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: doc.is_favorite ? t('Unpin') : t('Pin'),
|
||||
icon: 'push_pin',
|
||||
callback: () => {
|
||||
if (doc.is_favorite) {
|
||||
removeFavoriteDoc.mutate({ id: doc.id });
|
||||
} else {
|
||||
makeFavoriteDoc.mutate({ id: doc.id });
|
||||
}
|
||||
},
|
||||
testId: `docs-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`,
|
||||
},
|
||||
{
|
||||
label: t('Version history'),
|
||||
icon: 'history',
|
||||
disabled: !doc.abilities.versions_list,
|
||||
callback: () => {
|
||||
selectHistoryModal.open();
|
||||
},
|
||||
show: isDesktop,
|
||||
},
|
||||
|
||||
{
|
||||
label: t('Copy as {{format}}', { format: 'Markdown' }),
|
||||
icon: 'content_copy',
|
||||
callback: () => {
|
||||
void copyCurrentEditorToClipboard('markdown');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('Copy as {{format}}', { format: 'HTML' }),
|
||||
icon: 'content_copy',
|
||||
callback: () => {
|
||||
void copyCurrentEditorToClipboard('html');
|
||||
},
|
||||
show: isFeatureFlagActivated('CopyAsHTML'),
|
||||
},
|
||||
{
|
||||
label: t('Delete document'),
|
||||
icon: 'delete',
|
||||
disabled: !doc.abilities.destroy,
|
||||
callback: () => {
|
||||
setIsModalRemoveOpen(true);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const copyCurrentEditorToClipboard = async (
|
||||
asFormat: 'html' | 'markdown',
|
||||
) => {
|
||||
if (!editor) {
|
||||
toast(t('Editor unavailable'), VariantType.ERROR, { duration: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const editorContentFormatted =
|
||||
asFormat === 'html'
|
||||
? await editor.blocksToHTMLLossy()
|
||||
: await editor.blocksToMarkdownLossy();
|
||||
await navigator.clipboard.writeText(editorContentFormatted);
|
||||
toast(t('Copied to clipboard'), VariantType.SUCCESS, { duration: 3000 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast(t('Failed to copy to clipboard'), VariantType.ERROR, {
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
const { isSmallMobile } = useResponsiveStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (selectHistoryModal.isOpen) {
|
||||
if (modalHistory.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
void queryClient.resetQueries({
|
||||
queryKey: [KEY_LIST_DOC_VERSIONS],
|
||||
});
|
||||
}, [selectHistoryModal.isOpen, queryClient]);
|
||||
}, [modalHistory.isOpen, queryClient]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -222,55 +99,12 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isSmallMobile && (
|
||||
<Button
|
||||
color="tertiary-text"
|
||||
icon={
|
||||
<Icon iconName="download" $theme="primary" $variation="800" />
|
||||
}
|
||||
onClick={() => {
|
||||
setIsModalExportOpen(true);
|
||||
}}
|
||||
size={isSmallMobile ? 'small' : 'medium'}
|
||||
/>
|
||||
)}
|
||||
<DropdownMenu options={options}>
|
||||
<IconOptions
|
||||
isHorizontal
|
||||
$theme="primary"
|
||||
$padding={{ all: 'xs' }}
|
||||
$css={css`
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
background-color: ${colorsTokens['greyscale-100']};
|
||||
}
|
||||
${isSmallMobile
|
||||
? css`
|
||||
padding: 10px;
|
||||
border: 1px solid ${colorsTokens['greyscale-300']};
|
||||
`
|
||||
: ''}
|
||||
`}
|
||||
aria-label={t('Open the document options')}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</Box>
|
||||
|
||||
{modalShare.isOpen && (
|
||||
<DocShareModal onClose={() => modalShare.close()} doc={doc} />
|
||||
)}
|
||||
{isModalExportOpen && (
|
||||
<ModalExport onClose={() => setIsModalExportOpen(false)} doc={doc} />
|
||||
)}
|
||||
{isModalRemoveOpen && (
|
||||
<ModalRemoveDoc onClose={() => setIsModalRemoveOpen(false)} doc={doc} />
|
||||
)}
|
||||
{selectHistoryModal.isOpen && (
|
||||
<ModalSelectVersion
|
||||
onClose={() => selectHistoryModal.close()}
|
||||
<DocToolBoxLicence
|
||||
doc={doc}
|
||||
modalHistory={modalHistory}
|
||||
modalShare={modalShare}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { Button, useModal } from '@openfun/cunningham-react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuOption,
|
||||
Icon,
|
||||
IconOptions,
|
||||
} from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { ModalExport } from '@/docs/doc-export/';
|
||||
import {
|
||||
Doc,
|
||||
KEY_DOC,
|
||||
KEY_LIST_DOC,
|
||||
ModalRemoveDoc,
|
||||
useCopyDocLink,
|
||||
useCreateFavoriteDoc,
|
||||
useDeleteFavoriteDoc,
|
||||
} from '@/docs/doc-management';
|
||||
import {
|
||||
KEY_LIST_DOC_VERSIONS,
|
||||
ModalSelectVersion,
|
||||
} from '@/docs/doc-versioning';
|
||||
import { useAnalytics } from '@/libs';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocShareModal } from '../../doc-share';
|
||||
import { useCopyCurrentEditorToClipboard } from '../hooks/useCopyCurrentEditorToClipboard';
|
||||
|
||||
type ModalType = ReturnType<typeof useModal>;
|
||||
|
||||
interface DocToolBoxLicenceProps {
|
||||
doc: Doc;
|
||||
modalHistory: ModalType;
|
||||
modalShare: ModalType;
|
||||
}
|
||||
|
||||
export const DocToolBoxLicenceAGPL = ({
|
||||
doc,
|
||||
modalHistory,
|
||||
modalShare,
|
||||
}: DocToolBoxLicenceProps) => {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
|
||||
const [isModalExportOpen, setIsModalExportOpen] = useState(false);
|
||||
|
||||
const { isSmallMobile, isDesktop } = useResponsiveStore();
|
||||
const copyDocLink = useCopyDocLink(doc.id);
|
||||
const { isFeatureFlagActivated } = useAnalytics();
|
||||
const removeFavoriteDoc = useDeleteFavoriteDoc({
|
||||
listInvalideQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||
});
|
||||
const makeFavoriteDoc = useCreateFavoriteDoc({
|
||||
listInvalideQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||
});
|
||||
const copyCurrentEditorToClipboard = useCopyCurrentEditorToClipboard();
|
||||
|
||||
const options: DropdownMenuOption[] = [
|
||||
...(isSmallMobile
|
||||
? [
|
||||
{
|
||||
label: t('Share'),
|
||||
icon: 'group',
|
||||
callback: modalShare.open,
|
||||
},
|
||||
{
|
||||
label: t('Export'),
|
||||
icon: 'download',
|
||||
callback: () => {
|
||||
setIsModalExportOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('Copy link'),
|
||||
icon: 'add_link',
|
||||
callback: copyDocLink,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: doc.is_favorite ? t('Unpin') : t('Pin'),
|
||||
icon: 'push_pin',
|
||||
callback: () => {
|
||||
if (doc.is_favorite) {
|
||||
removeFavoriteDoc.mutate({ id: doc.id });
|
||||
} else {
|
||||
makeFavoriteDoc.mutate({ id: doc.id });
|
||||
}
|
||||
},
|
||||
testId: `docs-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`,
|
||||
},
|
||||
{
|
||||
label: t('Version history'),
|
||||
icon: 'history',
|
||||
disabled: !doc.abilities.versions_list,
|
||||
callback: () => {
|
||||
modalHistory.open();
|
||||
},
|
||||
show: isDesktop,
|
||||
},
|
||||
|
||||
{
|
||||
label: t('Copy as {{format}}', { format: 'Markdown' }),
|
||||
icon: 'content_copy',
|
||||
callback: () => {
|
||||
void copyCurrentEditorToClipboard('markdown');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('Copy as {{format}}', { format: 'HTML' }),
|
||||
icon: 'content_copy',
|
||||
callback: () => {
|
||||
void copyCurrentEditorToClipboard('html');
|
||||
},
|
||||
show: isFeatureFlagActivated('CopyAsHTML'),
|
||||
},
|
||||
{
|
||||
label: t('Delete document'),
|
||||
icon: 'delete',
|
||||
disabled: !doc.abilities.destroy,
|
||||
callback: () => {
|
||||
setIsModalRemoveOpen(true);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (modalHistory.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
void queryClient.resetQueries({
|
||||
queryKey: [KEY_LIST_DOC_VERSIONS],
|
||||
});
|
||||
}, [modalHistory.isOpen, queryClient]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isSmallMobile && (
|
||||
<Button
|
||||
color="tertiary-text"
|
||||
icon={<Icon iconName="download" $theme="primary" $variation="800" />}
|
||||
onClick={() => {
|
||||
setIsModalExportOpen(true);
|
||||
}}
|
||||
size={isSmallMobile ? 'small' : 'medium'}
|
||||
/>
|
||||
)}
|
||||
<DropdownMenu options={options}>
|
||||
<IconOptions
|
||||
isHorizontal
|
||||
$theme="primary"
|
||||
$padding={{ all: 'xs' }}
|
||||
$css={css`
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
background-color: ${colorsTokens['greyscale-100']};
|
||||
}
|
||||
${isSmallMobile
|
||||
? css`
|
||||
padding: 10px;
|
||||
border: 1px solid ${colorsTokens['greyscale-300']};
|
||||
`
|
||||
: ''}
|
||||
`}
|
||||
aria-label={t('Open the document options')}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
|
||||
{modalShare.isOpen && (
|
||||
<DocShareModal onClose={() => modalShare.close()} doc={doc} />
|
||||
)}
|
||||
{isModalExportOpen && (
|
||||
<ModalExport onClose={() => setIsModalExportOpen(false)} doc={doc} />
|
||||
)}
|
||||
{isModalRemoveOpen && (
|
||||
<ModalRemoveDoc onClose={() => setIsModalRemoveOpen(false)} doc={doc} />
|
||||
)}
|
||||
{modalHistory.isOpen && (
|
||||
<ModalSelectVersion onClose={() => modalHistory.close()} doc={doc} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,165 @@
|
||||
import { useModal } from '@openfun/cunningham-react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { DropdownMenu, DropdownMenuOption, IconOptions } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import {
|
||||
Doc,
|
||||
KEY_DOC,
|
||||
KEY_LIST_DOC,
|
||||
ModalRemoveDoc,
|
||||
useCopyDocLink,
|
||||
useCreateFavoriteDoc,
|
||||
useDeleteFavoriteDoc,
|
||||
} from '@/docs/doc-management';
|
||||
import {
|
||||
KEY_LIST_DOC_VERSIONS,
|
||||
ModalSelectVersion,
|
||||
} from '@/docs/doc-versioning';
|
||||
import { useAnalytics } from '@/libs';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocShareModal } from '../../doc-share';
|
||||
import { useCopyCurrentEditorToClipboard } from '../hooks/useCopyCurrentEditorToClipboard';
|
||||
|
||||
type ModalType = ReturnType<typeof useModal>;
|
||||
|
||||
interface DocToolBoxLicenceProps {
|
||||
doc: Doc;
|
||||
modalHistory: ModalType;
|
||||
modalShare: ModalType;
|
||||
}
|
||||
|
||||
export const DocToolBoxLicenceMIT = ({
|
||||
doc,
|
||||
modalHistory,
|
||||
modalShare,
|
||||
}: DocToolBoxLicenceProps) => {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
|
||||
|
||||
const { isSmallMobile, isDesktop } = useResponsiveStore();
|
||||
const copyDocLink = useCopyDocLink(doc.id);
|
||||
const { isFeatureFlagActivated } = useAnalytics();
|
||||
const removeFavoriteDoc = useDeleteFavoriteDoc({
|
||||
listInvalideQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||
});
|
||||
const makeFavoriteDoc = useCreateFavoriteDoc({
|
||||
listInvalideQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||
});
|
||||
const copyCurrentEditorToClipboard = useCopyCurrentEditorToClipboard();
|
||||
|
||||
const options: DropdownMenuOption[] = [
|
||||
...(isSmallMobile
|
||||
? [
|
||||
{
|
||||
label: t('Share'),
|
||||
icon: 'group',
|
||||
callback: modalShare.open,
|
||||
},
|
||||
{
|
||||
label: t('Copy link'),
|
||||
icon: 'add_link',
|
||||
callback: copyDocLink,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: doc.is_favorite ? t('Unpin') : t('Pin'),
|
||||
icon: 'push_pin',
|
||||
callback: () => {
|
||||
if (doc.is_favorite) {
|
||||
removeFavoriteDoc.mutate({ id: doc.id });
|
||||
} else {
|
||||
makeFavoriteDoc.mutate({ id: doc.id });
|
||||
}
|
||||
},
|
||||
testId: `docs-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`,
|
||||
},
|
||||
{
|
||||
label: t('Version history'),
|
||||
icon: 'history',
|
||||
disabled: !doc.abilities.versions_list,
|
||||
callback: () => {
|
||||
modalHistory.open();
|
||||
},
|
||||
show: isDesktop,
|
||||
},
|
||||
|
||||
{
|
||||
label: t('Copy as {{format}}', { format: 'Markdown' }),
|
||||
icon: 'content_copy',
|
||||
callback: () => {
|
||||
void copyCurrentEditorToClipboard('markdown');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('Copy as {{format}}', { format: 'HTML' }),
|
||||
icon: 'content_copy',
|
||||
callback: () => {
|
||||
void copyCurrentEditorToClipboard('html');
|
||||
},
|
||||
show: isFeatureFlagActivated('CopyAsHTML'),
|
||||
},
|
||||
{
|
||||
label: t('Delete document'),
|
||||
icon: 'delete',
|
||||
disabled: !doc.abilities.destroy,
|
||||
callback: () => {
|
||||
setIsModalRemoveOpen(true);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (modalHistory.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
void queryClient.resetQueries({
|
||||
queryKey: [KEY_LIST_DOC_VERSIONS],
|
||||
});
|
||||
}, [modalHistory.isOpen, queryClient]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu options={options}>
|
||||
<IconOptions
|
||||
isHorizontal
|
||||
$theme="primary"
|
||||
$padding={{ all: 'xs' }}
|
||||
$css={css`
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
background-color: ${colorsTokens['greyscale-100']};
|
||||
}
|
||||
${isSmallMobile
|
||||
? css`
|
||||
padding: 10px;
|
||||
border: 1px solid ${colorsTokens['greyscale-300']};
|
||||
`
|
||||
: ''}
|
||||
`}
|
||||
aria-label={t('Open the document options')}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
|
||||
{modalShare.isOpen && (
|
||||
<DocShareModal onClose={() => modalShare.close()} doc={doc} />
|
||||
)}
|
||||
{isModalRemoveOpen && (
|
||||
<ModalRemoveDoc onClose={() => setIsModalRemoveOpen(false)} doc={doc} />
|
||||
)}
|
||||
{modalHistory.isOpen && (
|
||||
<ModalSelectVersion onClose={() => modalHistory.close()} doc={doc} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useEditorStore } from '../../doc-editor';
|
||||
|
||||
export const useCopyCurrentEditorToClipboard = () => {
|
||||
const { editor } = useEditorStore();
|
||||
const { toast } = useToastProvider();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return async (asFormat: 'html' | 'markdown') => {
|
||||
if (!editor) {
|
||||
toast(t('Editor unavailable'), VariantType.ERROR, { duration: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const editorContentFormatted =
|
||||
asFormat === 'html'
|
||||
? await editor.blocksToHTMLLossy()
|
||||
: await editor.blocksToMarkdownLossy();
|
||||
await navigator.clipboard.writeText(editorContentFormatted);
|
||||
toast(t('Copied to clipboard'), VariantType.SUCCESS, { duration: 3000 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast(t('Failed to copy to clipboard'), VariantType.ERROR, {
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -38,11 +38,15 @@ setCacheNameDetails({
|
||||
});
|
||||
|
||||
/**
|
||||
* In development, use NetworkFirst strategy, in production use CacheFirst strategy
|
||||
* We will be able to test the application in development without having to clear the cache.
|
||||
* @param url
|
||||
* @param options
|
||||
* @returns strategy
|
||||
* Chooses the appropriate caching strategy based on the environment and request context.
|
||||
*
|
||||
* - In **development**, or for **API requests**, or **HTML pages**, it returns a `NetworkFirst` strategy
|
||||
* to prioritize fresh responses and ease debugging without needing to clear caches.
|
||||
* - In **production** (for non-API, non-HTML content), it returns a `CacheFirst` strategy
|
||||
* to favor performance and offline access.
|
||||
*
|
||||
* @param {NetworkFirstOptions | StrategyOptions} [options] - Configuration options for the caching strategy.
|
||||
* @returns {NetworkFirst | CacheFirst} The selected Workbox strategy instance.
|
||||
*/
|
||||
const getStrategy = (
|
||||
options?: NetworkFirstOptions | StrategyOptions,
|
||||
@@ -181,7 +185,7 @@ registerRoute(
|
||||
);
|
||||
|
||||
/**
|
||||
* Cache stategy static files images (images / svg)
|
||||
* Cache strategy static files images (images / svg)
|
||||
*/
|
||||
registerRoute(
|
||||
({ request }) => request.destination === 'image',
|
||||
@@ -197,7 +201,7 @@ registerRoute(
|
||||
);
|
||||
|
||||
/**
|
||||
* Cache stategy static files fonts
|
||||
* Cache strategy static files fonts
|
||||
*/
|
||||
googleFontsCache();
|
||||
registerRoute(
|
||||
@@ -214,7 +218,7 @@ registerRoute(
|
||||
);
|
||||
|
||||
/**
|
||||
* Cache stategy static files (css, js, workers)
|
||||
* Cache strategy static files (css, js, workers)
|
||||
*/
|
||||
registerRoute(
|
||||
({ request }) =>
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"Accessible to anyone": "Für alle zugänglich",
|
||||
"Accessible to authenticated users": "Für authentifizierte Benutzer zugänglich",
|
||||
"Add": "Hinzufügen",
|
||||
"Add a callout block": "Hebt schrift hervor",
|
||||
"Add a horizontal line": "Waagerechte Linie einfügen",
|
||||
"Address:": "Anschrift:",
|
||||
"Administrator": "Administrator",
|
||||
@@ -67,6 +68,7 @@
|
||||
"Available soon": "Bald verfügbar",
|
||||
"Banner image": "Bannerbild",
|
||||
"Beautify": "Verschönern",
|
||||
"Callout": "Hervorhebung",
|
||||
"Can't load this page, please check your internet connection.": "Diese Seite kann nicht geladen werden. Bitte überprüfen Sie Ihre Internetverbindung.",
|
||||
"Cancel": "Abbrechen",
|
||||
"Close the modal": "Pop up schliessen",
|
||||
@@ -284,6 +286,7 @@
|
||||
"Accessible to anyone": "Accesible para todos",
|
||||
"Accessible to authenticated users": "Accesible a usuarios autenticados",
|
||||
"Add": "Añadir",
|
||||
"Add a callout block": "Resaltar el texto para que destaque",
|
||||
"Add a horizontal line": "Añadir una línea horizontal",
|
||||
"Address:": "Dirección:",
|
||||
"Administrator": "Administrador",
|
||||
@@ -298,6 +301,7 @@
|
||||
"Available soon": "Próximamente disponible",
|
||||
"Banner image": "Imagen de portada",
|
||||
"Beautify": "Embellecer",
|
||||
"Callout": "Destacado",
|
||||
"Can't load this page, please check your internet connection.": "No se puede cargar esta página, por favor compruebe su conexión a Internet.",
|
||||
"Cancel": "Cancelar",
|
||||
"Close the modal": "Cerrar modal",
|
||||
@@ -508,6 +512,7 @@
|
||||
"Accessible to authenticated users": "Accessible aux utilisateurs authentifiés",
|
||||
"Add": "Ajouter",
|
||||
"Add a horizontal line": "Ajouter une ligne horizontale",
|
||||
"Add a callout block": "Faites ressortir du texte pour le mettre en évidence",
|
||||
"Address:": "Adresse :",
|
||||
"Administrator": "Administrateur",
|
||||
"All docs": "Tous les documents",
|
||||
@@ -521,6 +526,7 @@
|
||||
"Available soon": "Disponible prochainement",
|
||||
"Banner image": "Image de la bannière",
|
||||
"Beautify": "Embellir",
|
||||
"Callout": "Encadré",
|
||||
"Can't load this page, please check your internet connection.": "Impossible de charger cette page, veuillez vérifier votre connexion Internet.",
|
||||
"Cancel": "Annuler",
|
||||
"Close the modal": "Fermer la modale",
|
||||
@@ -914,6 +920,7 @@
|
||||
"Accessible to anyone": "Toegankelijk voor iedereen",
|
||||
"Accessible to authenticated users": "Toegankelijk voor geauthentiseerde gebruikers",
|
||||
"Add": "Voeg toe",
|
||||
"Add a callout block": "Laat je tekst opvallen",
|
||||
"Add a horizontal line": "Voeg horizontale lijn toe",
|
||||
"Address:": "Adres:",
|
||||
"Administrator": "Administrator",
|
||||
@@ -928,6 +935,7 @@
|
||||
"Available soon": "Binnenkort beschikbaar",
|
||||
"Banner image": "Banner afbeelding",
|
||||
"Beautify": "Maak mooier",
|
||||
"Callout": "Benadrukken",
|
||||
"Can't load this page, please check your internet connection.": "Kan deze pagina niet laden. Controleer je internetverbinding.",
|
||||
"Cancel": "Breek af",
|
||||
"Close the modal": "Sluit het venster",
|
||||
|
||||
@@ -25,14 +25,10 @@ export class PostHogAnalytic extends AbstractAnalytic {
|
||||
}
|
||||
|
||||
public isFeatureFlagActivated(flagName: string): boolean {
|
||||
if (
|
||||
return !(
|
||||
posthog.featureFlags.getFlags().includes(flagName) &&
|
||||
posthog.isFeatureEnabled(flagName) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,11 +40,7 @@ export function isSafeUrl(url: string): boolean {
|
||||
}
|
||||
|
||||
// Check for directory traversal attempts
|
||||
if (url.includes('..') || url.includes('../') || url.includes('..\\')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return !(url.includes('..') || url.includes('../') || url.includes('..\\'));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impress",
|
||||
"version": "3.2.0",
|
||||
"version": "3.2.1",
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
@@ -28,12 +28,13 @@
|
||||
"server:test": "yarn COLLABORATION_SERVER run test"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "22.15.3",
|
||||
"@types/react": "19.1.2",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.31.1",
|
||||
"@typescript-eslint/parser": "8.31.1",
|
||||
"@types/node": "22.15.12",
|
||||
"@types/react": "19.1.3",
|
||||
"@types/react-dom": "19.1.3",
|
||||
"@typescript-eslint/eslint-plugin": "8.32.0",
|
||||
"@typescript-eslint/parser": "8.32.0",
|
||||
"eslint": "8.57.0",
|
||||
"prosemirror-model": "1.25.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"typescript": "5.8.3",
|
||||
|
||||
@@ -56,6 +56,7 @@ const globalRules = {
|
||||
'error',
|
||||
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
|
||||
],
|
||||
'no-var': 'error',
|
||||
'react/jsx-curly-brace-presence': [
|
||||
'error',
|
||||
{ props: 'never', children: 'never', propElementValues: 'always' },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eslint-config-impress",
|
||||
"version": "3.2.0",
|
||||
"version": "3.2.1",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js ."
|
||||
@@ -17,7 +17,7 @@
|
||||
"eslint-plugin-jest": "28.11.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-playwright": "2.2.0",
|
||||
"eslint-plugin-prettier": "5.2.6",
|
||||
"eslint-plugin-prettier": "5.4.0",
|
||||
"eslint-plugin-testing-library": "7.1.1",
|
||||
"prettier": "3.5.3"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "packages-i18n",
|
||||
"version": "3.2.0",
|
||||
"version": "3.2.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"extract-translation": "yarn extract-translation:impress",
|
||||
|
||||
@@ -13,5 +13,5 @@ module.exports = {
|
||||
rules: {
|
||||
'@next/next/no-html-link-for-pages': 'off',
|
||||
},
|
||||
ignorePatterns: ['node_modules', '.eslintrc.js'],
|
||||
ignorePatterns: ['node_modules'],
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine AS y-provider-builder
|
||||
FROM node:24-alpine AS y-provider-builder
|
||||
|
||||
WORKDIR /home/frontend/
|
||||
|
||||
@@ -15,7 +15,7 @@ COPY ./src/frontend/servers/y-provider ./servers/y-provider
|
||||
WORKDIR /home/frontend/servers/y-provider
|
||||
RUN yarn build
|
||||
|
||||
FROM node:20-alpine AS y-provider
|
||||
FROM node:24-alpine AS y-provider
|
||||
|
||||
WORKDIR /home/frontend/
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var config = {
|
||||
const config = {
|
||||
rootDir: './__tests__',
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server-y-provider",
|
||||
"version": "3.2.0",
|
||||
"version": "3.2.1",
|
||||
"description": "Y.js provider for docs",
|
||||
"repository": "https://github.com/numerique-gouv/impress",
|
||||
"license": "MIT",
|
||||
@@ -18,8 +18,8 @@
|
||||
"dependencies": {
|
||||
"@blocknote/server-util": "0.29.1",
|
||||
"@hocuspocus/server": "2.15.2",
|
||||
"@sentry/node": "9.14.0",
|
||||
"@sentry/profiling-node": "9.14.0",
|
||||
"@sentry/node": "9.15.0",
|
||||
"@sentry/profiling-node": "9.15.0",
|
||||
"axios": "1.9.0",
|
||||
"cors": "2.8.5",
|
||||
"express": "5.1.0",
|
||||
@@ -44,8 +44,8 @@
|
||||
"supertest": "7.1.0",
|
||||
"ts-jest": "29.3.2",
|
||||
"ts-node": "10.9.2",
|
||||
"tsc-alias": "1.8.15",
|
||||
"tsc-alias": "1.8.16",
|
||||
"typescript": "*",
|
||||
"ws": "8.18.1"
|
||||
"ws": "8.18.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ import { logger } from '../utils';
|
||||
/**
|
||||
* init the collaboration server.
|
||||
*
|
||||
* @param port - The port on which the server listens.
|
||||
* @param serverSecret - The secret key for API authentication.
|
||||
* @returns An object containing the Express app, Hocuspocus server, and HTTP server instance.
|
||||
*/
|
||||
export const initServer = () => {
|
||||
|
||||
@@ -1238,11 +1238,16 @@
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emoji-mart/data@^1.2.1":
|
||||
"@emoji-mart/data@1.2.1", "@emoji-mart/data@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.2.1.tgz#0ad70c662e3bc603e23e7d98413bd1e64c4fcb6c"
|
||||
integrity sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==
|
||||
|
||||
"@emoji-mart/react@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@emoji-mart/react/-/react-1.1.1.tgz#ddad52f93a25baf31c5383c3e7e4c6e05554312a"
|
||||
integrity sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==
|
||||
|
||||
"@emotion/babel-plugin@^11.13.5":
|
||||
version "11.13.5"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz#eab8d65dbded74e0ecfd28dc218e75607c4e7bc0"
|
||||
@@ -1480,6 +1485,13 @@
|
||||
dependencies:
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
"@eslint-community/eslint-utils@^4.7.0":
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"
|
||||
integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1":
|
||||
version "4.12.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0"
|
||||
@@ -4417,19 +4429,19 @@
|
||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz#75dce8e972f90bba488e2b0cc677fb233aa357ab"
|
||||
integrity sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==
|
||||
|
||||
"@sentry-internal/browser-utils@9.14.0":
|
||||
version "9.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-9.14.0.tgz#d864b6c6c41fb409baa5df5080443f7c33bf7b86"
|
||||
integrity sha512-pDk9XUu9zf7lcT9QX0nTObPNp/y0xQyy1Dj+5/8TSB3vAfe0LQcooKGl/D1h7EoIXVHUozZk5JC/dH+gz6BXRg==
|
||||
"@sentry-internal/browser-utils@9.15.0":
|
||||
version "9.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-9.15.0.tgz#999f0205fd6dc380f35c4a1133c64454c06a0fc3"
|
||||
integrity sha512-tIM+9zXCefkInRiNmBkXKgkamRjEOlAcf768cBKlMWVOatfNrSEB0UEV7qkAYqnQGWkbPkHFMbFJxWptydLODw==
|
||||
dependencies:
|
||||
"@sentry/core" "9.14.0"
|
||||
"@sentry/core" "9.15.0"
|
||||
|
||||
"@sentry-internal/feedback@9.14.0":
|
||||
version "9.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-9.14.0.tgz#d003ad6cddee140f7e82dbf649cf47e40489f6f9"
|
||||
integrity sha512-D+PiEUWbDT0vqmaTiOs6OzXwVRVFgf7BCkFs48qsN9sAPwUgT+5zh2oo/rU2r0NrmMcvJVtSY+ezwPMk8BgGsg==
|
||||
"@sentry-internal/feedback@9.15.0":
|
||||
version "9.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-9.15.0.tgz#dd3194e93765521559645fea2f2f864e62156134"
|
||||
integrity sha512-jyN0r57WL8V5ViwZpiNvbIhF9I89jxn6mtIQcyV85EjIXDyzJmeTgxc/FIU0kcDVv6zso3qnGRJUxGK+GvoYZg==
|
||||
dependencies:
|
||||
"@sentry/core" "9.14.0"
|
||||
"@sentry/core" "9.15.0"
|
||||
|
||||
"@sentry-internal/node-cpu-profiler@^2.0.0":
|
||||
version "2.1.0"
|
||||
@@ -4439,37 +4451,37 @@
|
||||
detect-libc "^2.0.3"
|
||||
node-abi "^3.73.0"
|
||||
|
||||
"@sentry-internal/replay-canvas@9.14.0":
|
||||
version "9.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-9.14.0.tgz#30516c5555c6646401ea97540a459a064910b1c4"
|
||||
integrity sha512-GhCSqc0oNzRiLhQsi9LCXgUmIwdHdvzVIsX4fihoFYWfgWSSj5YLqeEkb3CMM8htM6vheSFzIbPLlRS8fjCrPQ==
|
||||
"@sentry-internal/replay-canvas@9.15.0":
|
||||
version "9.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-9.15.0.tgz#7ec2a8bb8f571b78506cdba208ee6ed00faa7f40"
|
||||
integrity sha512-a1/oiXwcW5OmILjD7/R2UEsPQWXJBUr0u388uCKDUGeyXLxBBbIJGS5E8oLwVQLVxhVJrODgxvT19z9OVcbn7g==
|
||||
dependencies:
|
||||
"@sentry-internal/replay" "9.14.0"
|
||||
"@sentry/core" "9.14.0"
|
||||
"@sentry-internal/replay" "9.15.0"
|
||||
"@sentry/core" "9.15.0"
|
||||
|
||||
"@sentry-internal/replay@9.14.0":
|
||||
version "9.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-9.14.0.tgz#491d12930175e814c097b9b6ba097f47b4f4770c"
|
||||
integrity sha512-wgt397/PtpfVQ9t779a0L+hGH3JN9doXv3+9Wj98MLWwhymvJBjpjCFUBLScO5iP6imewTbRqQHbq7XS7I+x1A==
|
||||
"@sentry-internal/replay@9.15.0":
|
||||
version "9.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-9.15.0.tgz#7c6522b6b9d410f0f4b0b963777d2894fc11967f"
|
||||
integrity sha512-lv6ENRmfeBuod6Tr1WgLeF0+wIIXlHWNAGofsaNUvm8UKS7USicFsQWKOZPk4UyjTfrEClPp2vx+o7aUcZS6TQ==
|
||||
dependencies:
|
||||
"@sentry-internal/browser-utils" "9.14.0"
|
||||
"@sentry/core" "9.14.0"
|
||||
"@sentry-internal/browser-utils" "9.15.0"
|
||||
"@sentry/core" "9.15.0"
|
||||
|
||||
"@sentry/babel-plugin-component-annotate@3.3.1":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.3.1.tgz#baecd89396cbb4659565a4e8efe7f0a71b19262a"
|
||||
integrity sha512-5GOxGT7lZN+I8A7Vp0rWY+726FDKEw8HnFiebe51rQrMbfGfCu2Aw9uSM0nT9OG6xhV6WvGccIcCszTPs4fUZQ==
|
||||
|
||||
"@sentry/browser@9.14.0":
|
||||
version "9.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-9.14.0.tgz#159ca710028891e1e9b160d491ddfb774cf1ed67"
|
||||
integrity sha512-acxFbFEei3hzKr/IW3OmkzHlwohRaRBG0872nIhLYV2f/BgZmR6eV5zrUoELMmt2cgoLmDYyfp1734OoplfDbw==
|
||||
"@sentry/browser@9.15.0":
|
||||
version "9.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-9.15.0.tgz#a690b2bd5fb7ca18789887424e4c0773fd8262c8"
|
||||
integrity sha512-ppHESKFVQFpAb3rQI2ateDkmMytVcvAWsjZrZ3hF9iEnO3iTIIu32ib5nqQUL4KKXZQovYnDrSlDcdv3ZwX/8Q==
|
||||
dependencies:
|
||||
"@sentry-internal/browser-utils" "9.14.0"
|
||||
"@sentry-internal/feedback" "9.14.0"
|
||||
"@sentry-internal/replay" "9.14.0"
|
||||
"@sentry-internal/replay-canvas" "9.14.0"
|
||||
"@sentry/core" "9.14.0"
|
||||
"@sentry-internal/browser-utils" "9.15.0"
|
||||
"@sentry-internal/feedback" "9.15.0"
|
||||
"@sentry-internal/replay" "9.15.0"
|
||||
"@sentry-internal/replay-canvas" "9.15.0"
|
||||
"@sentry/core" "9.15.0"
|
||||
|
||||
"@sentry/bundler-plugin-core@3.3.1":
|
||||
version "3.3.1"
|
||||
@@ -4539,35 +4551,35 @@
|
||||
"@sentry/cli-win32-i686" "2.42.2"
|
||||
"@sentry/cli-win32-x64" "2.42.2"
|
||||
|
||||
"@sentry/core@9.14.0":
|
||||
version "9.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-9.14.0.tgz#4d51bf0ff719a8dee821d31be4abf1afd73f6c6e"
|
||||
integrity sha512-OLfucnP3LAL5bxVNWc2RVOHCX7fk9Er5bWPCS+O5cPjqNUUz0HQHhVh2Vhei5C0kYZZM4vy4BQit5T9LrlOaNA==
|
||||
"@sentry/core@9.15.0":
|
||||
version "9.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-9.15.0.tgz#590f16a15596ce01db49d9d80b31cb18048ca9a4"
|
||||
integrity sha512-lBmo3bzzaYUesdzc2H5K3fajfXyUNuj5koqyFoCAI8rnt9CBl7SUc/P07+E5eipF8mxgiU3QtkI7ALzRQN8pqQ==
|
||||
|
||||
"@sentry/nextjs@9.14.0":
|
||||
version "9.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-9.14.0.tgz#ed42410432c17632fea8ce0fcc036c29a7588d1c"
|
||||
integrity sha512-uQnG7tPs1qX8OZ5lW3mpslAoN2+XiV2ZJ/3T+VtBatx9MFgEga8lMA/qgdkZP+Q139sroGlUe0tcUSDwwIzaVw==
|
||||
"@sentry/nextjs@9.15.0":
|
||||
version "9.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-9.15.0.tgz#de54a6f47693512b606d20b8caa104ca5734b92d"
|
||||
integrity sha512-lx/q1Uqe37MtNff8UIBL5G8SaHn48lDlZyQKrsTd+4txBwT2DsAnyR029n/ZQW5bc1/rLM/qebKLy76x+Xq0vA==
|
||||
dependencies:
|
||||
"@opentelemetry/api" "^1.9.0"
|
||||
"@opentelemetry/semantic-conventions" "^1.30.0"
|
||||
"@rollup/plugin-commonjs" "28.0.1"
|
||||
"@sentry-internal/browser-utils" "9.14.0"
|
||||
"@sentry/core" "9.14.0"
|
||||
"@sentry/node" "9.14.0"
|
||||
"@sentry/opentelemetry" "9.14.0"
|
||||
"@sentry/react" "9.14.0"
|
||||
"@sentry/vercel-edge" "9.14.0"
|
||||
"@sentry-internal/browser-utils" "9.15.0"
|
||||
"@sentry/core" "9.15.0"
|
||||
"@sentry/node" "9.15.0"
|
||||
"@sentry/opentelemetry" "9.15.0"
|
||||
"@sentry/react" "9.15.0"
|
||||
"@sentry/vercel-edge" "9.15.0"
|
||||
"@sentry/webpack-plugin" "3.3.1"
|
||||
chalk "3.0.0"
|
||||
resolve "1.22.8"
|
||||
rollup "4.35.0"
|
||||
stacktrace-parser "^0.1.10"
|
||||
|
||||
"@sentry/node@9.14.0":
|
||||
version "9.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-9.14.0.tgz#683d9dacc0d864710fe292e52e839e5fdc39c94f"
|
||||
integrity sha512-AWPc6O+zAdSgnsiKm6Nb1txyiKCOIBriJEONdXFSslgZgkm8EWAYRRHyS2Hkmnz0/5bEQ3jEffIw22qJuaHN+Q==
|
||||
"@sentry/node@9.15.0":
|
||||
version "9.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-9.15.0.tgz#f43954ed8ccdc1cb7d5c315b7d481c5f46300885"
|
||||
integrity sha512-K0LdJxIzYbbsbiT+1tKgNq6MUHuDs2DggBDcFEp3T+yXVJYN1AyalUli06Kmxq8yvou6hgLwWL4gjIcB1IQySA==
|
||||
dependencies:
|
||||
"@opentelemetry/api" "^1.9.0"
|
||||
"@opentelemetry/context-async-hooks" "^1.30.1"
|
||||
@@ -4600,42 +4612,42 @@
|
||||
"@opentelemetry/sdk-trace-base" "^1.30.1"
|
||||
"@opentelemetry/semantic-conventions" "^1.30.0"
|
||||
"@prisma/instrumentation" "6.6.0"
|
||||
"@sentry/core" "9.14.0"
|
||||
"@sentry/opentelemetry" "9.14.0"
|
||||
"@sentry/core" "9.15.0"
|
||||
"@sentry/opentelemetry" "9.15.0"
|
||||
import-in-the-middle "^1.13.0"
|
||||
|
||||
"@sentry/opentelemetry@9.14.0":
|
||||
version "9.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-9.14.0.tgz#7454f23a4add2098a1364b8eea966c057503223e"
|
||||
integrity sha512-NnHJjSQGpWaZ6+0QK9Xn1T3CTOM16Ij07VnSiGmVz3/IMsNC1/jndqc8p9BxEI+67XhZjOUUN0Ogpq1XRY7YeA==
|
||||
"@sentry/opentelemetry@9.15.0":
|
||||
version "9.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-9.15.0.tgz#1888c8a08e69a49345d6a161a6dbc04fe6c7744f"
|
||||
integrity sha512-gGOzgSxbuh4B4SlEonL1LFsazmeqL/fn5CIQqRG0UWWxdt6TKAMlj0ThIlGF3jSHW2eXdpvs+4E73uFEaHIqfg==
|
||||
dependencies:
|
||||
"@sentry/core" "9.14.0"
|
||||
"@sentry/core" "9.15.0"
|
||||
|
||||
"@sentry/profiling-node@9.14.0":
|
||||
version "9.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/profiling-node/-/profiling-node-9.14.0.tgz#367dcc184f026f263a6faf0080acc26ad4215635"
|
||||
integrity sha512-DDbjWdkb/Hh0SOgzONbufUW7oqyTaHQu+oLWQidNaSHk57M3P/Rz8klxIrYRygvlZJQpd5RKJMpT8G+ceUBz0A==
|
||||
"@sentry/profiling-node@9.15.0":
|
||||
version "9.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/profiling-node/-/profiling-node-9.15.0.tgz#04c7588b8f805f521178e2ef859a482ea21b1e80"
|
||||
integrity sha512-I5lU5XNYOTeULAUgeZg9Un+nKmwkDKCQ9R6B+RUkT73Lz+xI6miriEfPcMXbeOcTwU6XygKHpcm+0Ksi80QJYA==
|
||||
dependencies:
|
||||
"@sentry-internal/node-cpu-profiler" "^2.0.0"
|
||||
"@sentry/core" "9.14.0"
|
||||
"@sentry/node" "9.14.0"
|
||||
"@sentry/core" "9.15.0"
|
||||
"@sentry/node" "9.15.0"
|
||||
|
||||
"@sentry/react@9.14.0":
|
||||
version "9.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-9.14.0.tgz#1fa021b5c1a4171356ee391b8ef9652bcf076e59"
|
||||
integrity sha512-0dRfTorcInBjxVnis6Zv0+Jqex2OXFNQf+cQanKuC0IRkAhZyD2+UO2/v39sSmtvrHIcZRQ9fta8qKdhFUXCqg==
|
||||
"@sentry/react@9.15.0":
|
||||
version "9.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-9.15.0.tgz#1b0f3b79180bd4359afb4c68da1f19fd866ff643"
|
||||
integrity sha512-8nojSjiEd/EWIgoWVfkNIkBGL2yoFZoVMBUTcYlypsMnUHNko2RJItOBqZs5/DRBxuzfBKVt8PF+gkhQOm6mPg==
|
||||
dependencies:
|
||||
"@sentry/browser" "9.14.0"
|
||||
"@sentry/core" "9.14.0"
|
||||
"@sentry/browser" "9.15.0"
|
||||
"@sentry/core" "9.15.0"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
|
||||
"@sentry/vercel-edge@9.14.0":
|
||||
version "9.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/vercel-edge/-/vercel-edge-9.14.0.tgz#9e36070f4199161b2c7c31a81f0f905d83297701"
|
||||
integrity sha512-qcQddIRNFJbDaBuGFfEthwMgR7+wsPfMVwMurQdxS7Axr3GgT03wVG/SPAVS/yhVLeqXrfeHaqJaNg05z0aErA==
|
||||
"@sentry/vercel-edge@9.15.0":
|
||||
version "9.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/vercel-edge/-/vercel-edge-9.15.0.tgz#e9e89dd922f5d322bcf42592cfbb47e4c0e5b771"
|
||||
integrity sha512-Rfc6pDbHMg5DMIgyZHVIO4IeHgxcH3myPBy9HP1hMLtcEqKL/YS8dK3oQrZoUsNP9chjXkrp4bBeKT/phX3pMg==
|
||||
dependencies:
|
||||
"@opentelemetry/api" "^1.9.0"
|
||||
"@sentry/core" "9.14.0"
|
||||
"@sentry/core" "9.15.0"
|
||||
|
||||
"@sentry/webpack-plugin@3.3.1":
|
||||
version "3.3.1"
|
||||
@@ -4869,29 +4881,29 @@
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "^8.18.1"
|
||||
|
||||
"@tanstack/query-core@5.74.9":
|
||||
version "5.74.9"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.74.9.tgz#35d5b1075663072bea22aa3ce21508b195306ecd"
|
||||
integrity sha512-qmjXpWyigDw4SfqdSBy24FzRvpBPXlaSbl92N77lcrL+yvVQLQkf0T6bQNbTxl9IEB/SvVFhhVZoIlQvFnNuuw==
|
||||
"@tanstack/query-core@5.75.4":
|
||||
version "5.75.4"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.75.4.tgz#e05f2fe4145fb5354271ad19e63eec61f6ce3012"
|
||||
integrity sha512-pcqOUgWG9oGlzkfRQQMMsEFmtQu0wq81A414CtELZGq+ztVwSTAaoB3AZRAXQJs88LmNMk2YpUKuQbrvzNDyRg==
|
||||
|
||||
"@tanstack/query-devtools@5.74.7":
|
||||
version "5.74.7"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.74.7.tgz#c9b022b386ac86e6395228b5d6912e6444b3b971"
|
||||
integrity sha512-nSNlfuGdnHf4yB0S+BoNYOE1o3oAH093weAYZolIHfS2stulyA/gWfSk/9H4ZFk5mAAHb5vNqAeJOmbdcGPEQw==
|
||||
|
||||
"@tanstack/react-query-devtools@5.74.9":
|
||||
version "5.74.9"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.74.9.tgz#7d79e27b2c4d949c9c1e269eafdb7c2256f6d493"
|
||||
integrity sha512-6dMfeK/5OvC9E88/ziwiv1Pggqkgjker8V+pLJFrjh7O7E7S6yXJRNNr/KjA/c+z6d/i7HpDk8FF+oSr7mhYLg==
|
||||
"@tanstack/react-query-devtools@5.75.4":
|
||||
version "5.75.4"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.75.4.tgz#89614363d63c997ade81ade4a18e90b57512d4d8"
|
||||
integrity sha512-CSJZWa316EFtLZtr6RQLAnqWb1MESzyZ7j0bMQjuhYas5FDp/3MA7G6RE4hWauqCCDsNIfIm2Rnm1zJTZVye/w==
|
||||
dependencies:
|
||||
"@tanstack/query-devtools" "5.74.7"
|
||||
|
||||
"@tanstack/react-query@5.74.9":
|
||||
version "5.74.9"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.74.9.tgz#fdcac4a31cd7f5786eda7526ee70ed6c63d13799"
|
||||
integrity sha512-F8xCXDQRDgsPzLzX9+d6ycNoITAIU2bycc1idZd06bt/GjN1quEJDjHvEDWZGoVn0A/ZmntVrYv6TE0kR7c7LA==
|
||||
"@tanstack/react-query@5.75.4":
|
||||
version "5.75.4"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.75.4.tgz#721e1cdf7debb110671f558dc2b6276f637554a5"
|
||||
integrity sha512-Vf65pzYRkf8fk9SP1ncIZjvaXszBhtsvpf+h45Y/9kOywOrVZfBGUpCdffdsVzbmBzmz6TCFes9bM0d3pRrIsA==
|
||||
dependencies:
|
||||
"@tanstack/query-core" "5.74.9"
|
||||
"@tanstack/query-core" "5.75.4"
|
||||
|
||||
"@tanstack/react-table@8.20.6":
|
||||
version "8.20.6"
|
||||
@@ -5375,10 +5387,10 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*", "@types/node@22.10.7", "@types/node@22.15.3", "@types/node@^22.7.5":
|
||||
version "22.15.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.3.tgz#b7fb9396a8ec5b5dfb1345d8ac2502060e9af68b"
|
||||
integrity sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==
|
||||
"@types/node@*", "@types/node@22.10.7", "@types/node@22.15.12", "@types/node@^22.7.5":
|
||||
version "22.15.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.12.tgz#9ce54e51e09536faa94e4ec300c4728ee83bfa85"
|
||||
integrity sha512-K0fpC/ZVeb8G9rm7bH7vI0KAec4XHEhBam616nVJCV51bKzJ6oA3luG4WdKoaztxe70QaNjS/xBmcDLmr4PiGw==
|
||||
dependencies:
|
||||
undici-types "~6.21.0"
|
||||
|
||||
@@ -5434,10 +5446,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
|
||||
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
|
||||
|
||||
"@types/react-dom@*", "@types/react-dom@19.1.2":
|
||||
version "19.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.2.tgz#bd1fe3b8c28a3a2e942f85314dcfb71f531a242f"
|
||||
integrity sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==
|
||||
"@types/react-dom@*", "@types/react-dom@19.1.3":
|
||||
version "19.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.3.tgz#3f0c60804441bf34d19f8dd0d44405c0c0e21bfa"
|
||||
integrity sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==
|
||||
|
||||
"@types/react-modal@3.16.3":
|
||||
version "3.16.3"
|
||||
@@ -5451,10 +5463,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044"
|
||||
integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==
|
||||
|
||||
"@types/react@*", "@types/react@19.1.2":
|
||||
version "19.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.2.tgz#11df86f66f188f212c90ecb537327ec68bfd593f"
|
||||
integrity sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==
|
||||
"@types/react@*", "@types/react@19.1.3":
|
||||
version "19.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.3.tgz#c75a24b775a63280b02c66a55a3cfa04f4022cf7"
|
||||
integrity sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==
|
||||
dependencies:
|
||||
csstype "^3.0.2"
|
||||
|
||||
@@ -5564,30 +5576,30 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@*", "@typescript-eslint/eslint-plugin@8.31.1", "@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz#62f1befe59647524994e89de4516d8dcba7a850a"
|
||||
integrity sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==
|
||||
"@typescript-eslint/eslint-plugin@*", "@typescript-eslint/eslint-plugin@8.32.0", "@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
version "8.32.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz#86630dd3084f9d6c4239bbcd6a7ee1a7ee844f7f"
|
||||
integrity sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "8.31.1"
|
||||
"@typescript-eslint/type-utils" "8.31.1"
|
||||
"@typescript-eslint/utils" "8.31.1"
|
||||
"@typescript-eslint/visitor-keys" "8.31.1"
|
||||
"@typescript-eslint/scope-manager" "8.32.0"
|
||||
"@typescript-eslint/type-utils" "8.32.0"
|
||||
"@typescript-eslint/utils" "8.32.0"
|
||||
"@typescript-eslint/visitor-keys" "8.32.0"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.3.1"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.0.1"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/parser@*", "@typescript-eslint/parser@8.31.1", "@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.31.1.tgz#e9b0ccf30d37dde724ee4d15f4dbc195995cce1b"
|
||||
integrity sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==
|
||||
"@typescript-eslint/parser@*", "@typescript-eslint/parser@8.32.0", "@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
version "8.32.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.32.0.tgz#fe840ecb2726a82fa9f5562837ec40503ae71caf"
|
||||
integrity sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.31.1"
|
||||
"@typescript-eslint/types" "8.31.1"
|
||||
"@typescript-eslint/typescript-estree" "8.31.1"
|
||||
"@typescript-eslint/visitor-keys" "8.31.1"
|
||||
"@typescript-eslint/scope-manager" "8.32.0"
|
||||
"@typescript-eslint/types" "8.32.0"
|
||||
"@typescript-eslint/typescript-estree" "8.32.0"
|
||||
"@typescript-eslint/visitor-keys" "8.32.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.31.1", "@typescript-eslint/scope-manager@^8.15.0":
|
||||
@@ -5598,21 +5610,34 @@
|
||||
"@typescript-eslint/types" "8.31.1"
|
||||
"@typescript-eslint/visitor-keys" "8.31.1"
|
||||
|
||||
"@typescript-eslint/type-utils@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz#be0f438fb24b03568e282a0aed85f776409f970c"
|
||||
integrity sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==
|
||||
"@typescript-eslint/scope-manager@8.32.0":
|
||||
version "8.32.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz#6be89f652780f0d3d19d58dc0ee107b1a9e3282c"
|
||||
integrity sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "8.31.1"
|
||||
"@typescript-eslint/utils" "8.31.1"
|
||||
"@typescript-eslint/types" "8.32.0"
|
||||
"@typescript-eslint/visitor-keys" "8.32.0"
|
||||
|
||||
"@typescript-eslint/type-utils@8.32.0":
|
||||
version "8.32.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz#5e0882393e801963f749bea38888e716045fe895"
|
||||
integrity sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "8.32.0"
|
||||
"@typescript-eslint/utils" "8.32.0"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.0.1"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/types@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.31.1.tgz#478ed6f7e8aee1be7b63a60212b6bffe1423b5d4"
|
||||
integrity sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==
|
||||
|
||||
"@typescript-eslint/types@8.32.0":
|
||||
version "8.32.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.32.0.tgz#a4a66b8876b8391970cf069b49572e43f1fc957a"
|
||||
integrity sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz#37792fe7ef4d3021c7580067c8f1ae66daabacdf"
|
||||
@@ -5627,7 +5652,31 @@
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.0.1"
|
||||
|
||||
"@typescript-eslint/utils@8.31.1", "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/utils@^8.15.0", "@typescript-eslint/utils@^8.18.1":
|
||||
"@typescript-eslint/typescript-estree@8.32.0":
|
||||
version "8.32.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz#11d45f47bfabb141206a3da6c7b91a9d869ff32d"
|
||||
integrity sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.32.0"
|
||||
"@typescript-eslint/visitor-keys" "8.32.0"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
minimatch "^9.0.4"
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/utils@8.32.0":
|
||||
version "8.32.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.32.0.tgz#24570f68cf845d198b73a7f94ca88d8c2505ba47"
|
||||
integrity sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.7.0"
|
||||
"@typescript-eslint/scope-manager" "8.32.0"
|
||||
"@typescript-eslint/types" "8.32.0"
|
||||
"@typescript-eslint/typescript-estree" "8.32.0"
|
||||
|
||||
"@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/utils@^8.15.0", "@typescript-eslint/utils@^8.18.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.31.1.tgz#5628ea0393598a0b2f143d0fc6d019f0dee9dd14"
|
||||
integrity sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==
|
||||
@@ -5645,6 +5694,14 @@
|
||||
"@typescript-eslint/types" "8.31.1"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.32.0":
|
||||
version "8.32.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz#0cca2cac046bc71cc40ce8214bac2850d6ecf4a6"
|
||||
integrity sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.32.0"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8"
|
||||
@@ -7410,7 +7467,7 @@ emittery@^0.13.1:
|
||||
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad"
|
||||
integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==
|
||||
|
||||
emoji-mart@^5.6.0:
|
||||
emoji-mart@5.6.0, emoji-mart@^5.6.0:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.6.0.tgz#71b3ed0091d3e8c68487b240d9d6d9a73c27f023"
|
||||
integrity sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==
|
||||
@@ -7787,10 +7844,10 @@ eslint-plugin-playwright@2.2.0:
|
||||
dependencies:
|
||||
globals "^13.23.0"
|
||||
|
||||
eslint-plugin-prettier@5.2.6:
|
||||
version "5.2.6"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz#be39e3bb23bb3eeb7e7df0927cdb46e4d7945096"
|
||||
integrity sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==
|
||||
eslint-plugin-prettier@5.4.0:
|
||||
version "5.4.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz#54d4748904e58eaf1ffe26c4bffa4986ca7f952b"
|
||||
integrity sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==
|
||||
dependencies:
|
||||
prettier-linter-helpers "^1.0.0"
|
||||
synckit "^0.11.0"
|
||||
@@ -8964,10 +9021,10 @@ hyphen@^1.6.4:
|
||||
resolved "https://registry.yarnpkg.com/hyphen/-/hyphen-1.10.6.tgz#0e779d280e696102b97d7e42f5ca5de2cc97e274"
|
||||
integrity sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==
|
||||
|
||||
i18next-browser-languagedetector@8.0.5:
|
||||
version "8.0.5"
|
||||
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.5.tgz#6cfdc72820457ce95e69a2788a4f837d1d8f4e9d"
|
||||
integrity sha512-OstebRKqKiQw8xEvQF5aRyUujsCatanj7Q9eo5iiH2gJpoXGZ7483ol3sVBwfqbobTQPNH1J+NAyJ1aCQoEC+w==
|
||||
i18next-browser-languagedetector@8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.1.0.tgz#e7fcc1084e9ca8fece329c8c05b000a6e25bd82e"
|
||||
integrity sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.23.2"
|
||||
|
||||
@@ -8994,10 +9051,10 @@ i18next-parser@9.3.0:
|
||||
vinyl "^3.0.0"
|
||||
vinyl-fs "^4.0.0"
|
||||
|
||||
i18next@25.0.2:
|
||||
version "25.0.2"
|
||||
resolved "https://registry.yarnpkg.com/i18next/-/i18next-25.0.2.tgz#3bfd51d11603e130cc4a88ff37038393dc560951"
|
||||
integrity sha512-xWxgK8GAaPYkV9ia2tdgbtdM+qiC+ysVTBPvXhpCORU/+QkeQe3BSI7Crr+c4ZXULN1PfnXG/HY2n7HGx4KKBg==
|
||||
i18next@25.1.1:
|
||||
version "25.1.1"
|
||||
resolved "https://registry.yarnpkg.com/i18next/-/i18next-25.1.1.tgz#113e7773697d300e7e824185f766362eceb8dc61"
|
||||
integrity sha512-FZcp3vk3PXc8onasbsWYahfeDIWX4LkKr4vd01xeXrmqyNXlVNtVecEIw2K1o8z3xYrHMcd1bwYQub+3g7zqCw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.26.10"
|
||||
|
||||
@@ -11565,10 +11622,10 @@ postgres-range@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863"
|
||||
integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==
|
||||
|
||||
posthog-js@1.236.8:
|
||||
version "1.236.8"
|
||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.236.8.tgz#0576c6ffc17ba76f869e82dfda82dccaab084479"
|
||||
integrity sha512-Nmd1LP9lAairJ2gkIIHNHeY8c7F6nO0g6OWaAHe6MfRnKWwnzBoLkDXEX4ptvooQVKnQ3eS7UR0Q+112TAn9qw==
|
||||
posthog-js@1.239.1:
|
||||
version "1.239.1"
|
||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.239.1.tgz#871c734a438f2ead1529deba26031d689819d587"
|
||||
integrity sha512-4cN3A02e9uyyTVQ/9+bGVfvoAmpM1mlVXlq2DsujCQjloMMSbEb+6Uwv6fro3Q5qk7NEU62kQqZHyvqQIqv9zA==
|
||||
dependencies:
|
||||
core-js "^3.38.1"
|
||||
fflate "^0.4.8"
|
||||
@@ -11749,10 +11806,10 @@ prosemirror-menu@^1.2.4:
|
||||
prosemirror-history "^1.0.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
|
||||
prosemirror-model@^1.0.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.23.0, prosemirror-model@^1.24.1, prosemirror-model@^1.25.0:
|
||||
version "1.25.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.25.1.tgz#aeae9f1ec79fcaa76f6fc619800d91fbcf726870"
|
||||
integrity sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==
|
||||
prosemirror-model@1.25.0, prosemirror-model@^1.0.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.23.0, prosemirror-model@^1.24.1, prosemirror-model@^1.25.0:
|
||||
version "1.25.0"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.25.0.tgz#c147113edc0718a14f03881e4c20367d0221f7af"
|
||||
integrity sha512-/8XUmxWf0pkj2BmtqZHYJipTBMHIdVjuvFzMvEoxrtyGNmfvdhBiRwYt/eFwy2wA9DtBW3RLqvZnjurEkHaFCw==
|
||||
dependencies:
|
||||
orderedmap "^2.0.0"
|
||||
|
||||
@@ -13830,7 +13887,7 @@ trough@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f"
|
||||
integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==
|
||||
|
||||
ts-api-utils@^2.0.1:
|
||||
ts-api-utils@^2.0.1, ts-api-utils@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91"
|
||||
integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
|
||||
@@ -13870,10 +13927,10 @@ ts-node@10.9.2:
|
||||
v8-compile-cache-lib "^3.0.1"
|
||||
yn "3.1.1"
|
||||
|
||||
tsc-alias@1.8.15:
|
||||
version "1.8.15"
|
||||
resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.8.15.tgz#7a07a77a4157872f834841a2a1647fad9464884d"
|
||||
integrity sha512-yKLVx8ddUurRwhVcS6JFF2ZjksOX2ZWDRIdgt+PQhJBDegIdAdilptiHsuAbx9UFxa16GFrxeKQ2kTcGvR6fkQ==
|
||||
tsc-alias@1.8.16:
|
||||
version "1.8.16"
|
||||
resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.8.16.tgz#dbc74e797071801c7284f1a478259de920f852d4"
|
||||
integrity sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==
|
||||
dependencies:
|
||||
chokidar "^3.5.3"
|
||||
commander "^9.0.0"
|
||||
@@ -14861,16 +14918,21 @@ write-file-atomic@^5.0.1:
|
||||
imurmurhash "^0.1.4"
|
||||
signal-exit "^4.0.1"
|
||||
|
||||
ws@8.18.1, ws@^8.11.0, ws@^8.17.1, ws@^8.18.0, ws@^8.5.0:
|
||||
version "8.18.1"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb"
|
||||
integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==
|
||||
ws@8.18.2:
|
||||
version "8.18.2"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a"
|
||||
integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==
|
||||
|
||||
ws@^7.4.6:
|
||||
version "7.5.10"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
|
||||
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
|
||||
|
||||
ws@^8.11.0, ws@^8.17.1, ws@^8.18.0, ws@^8.5.0:
|
||||
version "8.18.1"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb"
|
||||
integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==
|
||||
|
||||
xml-js@^1.6.8:
|
||||
version "1.6.11"
|
||||
resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9"
|
||||
@@ -14972,10 +15034,10 @@ yoga-layout@^3.2.1:
|
||||
resolved "https://registry.yarnpkg.com/yoga-layout/-/yoga-layout-3.2.1.tgz#d2d1ba06f0e81c2eb650c3e5ad8b0b4adde1e843"
|
||||
integrity sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==
|
||||
|
||||
zustand@5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.3.tgz#b323435b73d06b2512e93c77239634374b0e407f"
|
||||
integrity sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==
|
||||
zustand@5.0.4:
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.4.tgz#33af161f1e337854ccd8b711ef9e92545d6ae53f"
|
||||
integrity sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ==
|
||||
|
||||
zwitch@^2.0.0, zwitch@^2.0.4:
|
||||
version "2.0.4"
|
||||
|
||||
123
src/helm/env.d/dev/configuration/theme/demo.json
Normal file
123
src/helm/env.d/dev/configuration/theme/demo.json
Normal file
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ backend:
|
||||
POSTGRES_USER: dinum
|
||||
POSTGRES_PASSWORD: pass
|
||||
REDIS_URL: redis://default:pass@redis-master:6379/1
|
||||
DJANGO_CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
|
||||
AWS_S3_ENDPOINT_URL: http://minio.impress.svc.cluster.local:9000
|
||||
AWS_S3_ACCESS_KEY_ID: root
|
||||
AWS_S3_SECRET_ACCESS_KEY: password
|
||||
@@ -63,6 +64,7 @@ backend:
|
||||
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
||||
Y_PROVIDER_API_BASE_URL: http://impress-y-provider:443/api/
|
||||
Y_PROVIDER_API_KEY: my-secret
|
||||
CACHES_KEY_PREFIX: "{{ now | unixEpoch }}"
|
||||
migrate:
|
||||
command:
|
||||
- "/bin/sh"
|
||||
@@ -87,13 +89,17 @@ backend:
|
||||
python manage.py createsuperuser --email admin@example.com --password admin
|
||||
restartPolicy: Never
|
||||
|
||||
themeCustomization:
|
||||
enabled: true
|
||||
file_content: {{ readFile "./configuration/theme/demo.json" }}
|
||||
|
||||
# Extra volume mounts to manage our local custom CA and avoid to set ssl_verify: false
|
||||
extraVolumeMounts:
|
||||
- name: certs
|
||||
mountPath: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
|
||||
subPath: cacert.pem
|
||||
|
||||
# Exra volumes to manage our local custom CA and avoid to set ssl_verify: false
|
||||
# Extra volumes to manage our local custom CA and avoid to set ssl_verify: false
|
||||
extraVolumes:
|
||||
- name: certs
|
||||
configMap:
|
||||
@@ -101,7 +107,6 @@ backend:
|
||||
items:
|
||||
- key: cacert.pem
|
||||
path: cacert.pem
|
||||
|
||||
frontend:
|
||||
envVars:
|
||||
PORT: 8080
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user