Compare commits

...

72 Commits

Author SHA1 Message Date
Nathan Vasse eefb7d92ea wip: dsfr 2024-09-13 11:44:56 +02:00
Emmanuel Pelletier 872cece241 💄(frontend) opendesk theme: fix datagrid actions being white 2024-09-13 10:49:36 +02:00
Emmanuel Pelletier 91728a3ee1 💄(frontend) opendesk theme: have rounded borders generally 2024-09-13 10:49:24 +02:00
Nathan Vasse 439eb0aadb wip 2024-09-13 10:29:39 +02:00
René Fischer 8556757a8a Add German translation 2024-09-13 10:29:39 +02:00
Nathan Vasse f6da4892e2 wip 2024-09-13 10:29:39 +02:00
Emmanuel Pelletier 5333188177 💄(frontend) fix opensans sensitive case 2024-09-12 18:06:17 +02:00
Nathan Vasse 53a306405f wip 2024-09-12 18:02:41 +02:00
Nathan Vasse b600d598b4 wip 2024-09-12 17:28:19 +02:00
Nathan Vasse aeb7096719 wip 2024-09-12 17:28:19 +02:00
Nathan Vasse 5106292964 wip 2024-09-12 17:28:19 +02:00
Emmanuel Pelletier 65b7632a6d 💄(frontend) tiny hotfixes for opendesk theme 2024-09-12 17:28:07 +02:00
Emmanuel Pelletier e9f7d57227 💄(frontend) using darker primary color variation on important items
this is mainly to make opendesk demo work better, will clean that so
that it's customizable by theme
2024-09-12 17:17:59 +02:00
Emmanuel Pelletier 50ce4523bd 💄(frontend) doc dropmenu items: fix white text on white background
this is due to tokens update before
2024-09-12 17:05:03 +02:00
Nathan Vasse c306831b13 wip 2024-09-12 16:44:46 +02:00
Nathan Vasse a7aeedb8c2 wip 2024-09-12 16:44:40 +02:00
Nathan Vasse fb9cea586c wip 2024-09-12 16:16:00 +02:00
Nathan Vasse cc7747089f wip 2024-09-12 16:16:00 +02:00
Emmanuel Pelletier 9e8376a27c 💄(frontend) cleanup Marianne font files
- to improve performances and to encourage to have a less "spread out"
design accross the app, we should avoid using loads of font weight
(light, extra bold, light italic, etc.).

I suggest removing all together the unused font weights for now, and
removing from the tokens the extraneous weights.

This makes the app globally faster to load. Also, we use woff2 format
that is widely supported since years now, instead of woff2, to gain a
bit of weight.
2024-09-12 15:23:09 +02:00
Emmanuel Pelletier 73bb935c54 💄(frontend) better usage of Open Sans files for openDesk theme
- remove all the tff files
- use only woff2 as it is supported everyhwere
- only load common font weights (regular, italic, bold), we should not
need more, we'll see

for now fonts from all themes are loaded… this should be improved at
some point
2024-09-12 15:14:10 +02:00
Nathan Vasse 6875e2f722 wip 2024-09-12 14:49:32 +02:00
Nathan Vasse a009f8e35e wip 2024-09-12 14:14:00 +02:00
Nathan Vasse 4045969703 wip 2024-09-12 13:09:10 +02:00
Nathan Vasse 54d76a40d0 wip 2024-09-12 12:32:48 +02:00
Nathan Vasse 247af951b9 💄(front) add OpenDesk theme to Cunningham
WIP
2024-09-12 12:02:57 +02:00
Anthony LC b716881d50 📌(frontend) add lacking peerdependencies
- Add some lacking peerdependencies to the packages.
- Resolve conflicts eslint dependencies.
2024-09-11 11:39:03 +02:00
renovate[bot] fa8466d44d ⬆️(dependencies) update js dependencies 2024-09-11 11:39:03 +02:00
Anthony LC 4ba34f6c80 (CI) add ngnix for the frontend
Because of the Next.js bug with the 404 on the
dynamic routes, we are not able to assert some
behaviors from the e2e tests and the CI.
So we are adding a ngnix to the CI e2e tests
to be able to route correctly our frontend.
2024-09-10 15:51:28 +02:00
Anthony LC e4712831f2 🎨(frontend) standalone component DocTagPublic
We want to rerender the public tag when we update
the visibility of a document. The problem is that
the public tag is not a standalone component, so
to have it rerender we needed to rerender the whole
document, it is not visually nice.
We created a standalone component for
the public tag, so when we update the visibility
of a document, only the public tag will be rerender.
2024-09-10 15:51:28 +02:00
Anthony LC 37db31a8d5 (frontend) add copy link button
Add a copy link button to the doc
visibility component. This button will
copy the link of the doc to the clipboard.
2024-09-10 15:51:28 +02:00
Anthony LC 4321511631 🚚(frontend) change visibility in share modal
We stop to propose to make the document public
from the doc creation modal.
We now propose to change the visibility of
the document from the share modal.
2024-09-10 15:51:28 +02:00
Anthony LC 459cb5e2e2 🛂(frontend) access public docs without being logged
We can now access public docs without being logged.
2024-09-10 15:51:28 +02:00
Anthony LC 2a7e3116bd 🔖(minor) release 1.3.0
Added:
- Add image attachments with access control
- (frontend) Upload image to a document
- (frontend) Summary
- (frontend) update meta title for docs page

Changed:
- 💄(frontend) code background darkened on editor
- 🔥(frontend) hide markdown button if not text

Fixed:
- 🐛 Fix emoticon in pdf export
- 🐛 Fix collaboration on document
- 🐛 (docker) Fix compatibility with mac

Removed:
- 🔥(frontend) remove saving modal
2024-09-10 09:04:54 +02:00
Anthony LC b9046a2d9b 🐛(frontend) meta title rerender issue
The meta title is not displayed when we come back to
a page from the dynamic router. The code seems to
compute to quickly so we need to add a delay to the
meta title computation.
2024-09-09 17:47:24 +02:00
Anthony LC d249ed0c71 🔧(helm) change production media storage name
The set the correct media storage name
for production environment.
2024-09-09 16:36:24 +02:00
Anthony LC 48d3738ec2 (frontend) update meta title for docs page
We update the meta title for the docs page
with the title of the document.
It will be easier for the user
to identify the document in their browser tab,
in their bookmarks and history.
2024-09-05 13:26:49 +02:00
Anthony LC 92102e4a36 🔧(compose) stop forcing platform for Keycloak PostgreSQL image
Forcing `platform: linux/amd64` for the PostgreSQL
image causes compatibility issues and performance
degradation on Mac ARM chips (M1/M2).
Removing the platform specification allows Docker
to select the appropriate architecture automatically,
ensuring better performance and compatibility.
2024-09-05 12:09:15 +02:00
Anthony LC dd1b271b71 🌐(frontend) translate last features
Translate:
- doc versions
- doc summary
- export docx
2024-09-05 09:30:56 +02:00
Anthony LC 7cfc1d8036 ⬆️(i18n) i18next-parser to 9.0.2
i18next-parser had a compatibility issue with
a dependency (cheerio). The last version
fixed this issue, plus fixed another issue
about a configuration problem.
We can now remove it from the renovate ignore list.
2024-09-05 09:30:56 +02:00
Anthony LC 86fdbeacaa 🔥(frontend) do not display feature version
A bug was found in the version feature.
A 404 error appears sometimes, probably because
of Minio that does not keep enough versions.
We want to do a realease, so we will remove the
version feature for now.
2024-09-05 09:30:56 +02:00
Anthony LC 520d511f59 🔧(project) replace webrtc by yProvider
Replace webrtc by yProvider the project
(docker, helm chart, etc).
2024-09-04 21:10:24 +02:00
Anthony LC 9c512fae69 ♻️(y-provider) replace y-webrtc-signaling by server-y-provider
We replace the y-webrtc-signaling app by
the server-y-provider server.
The server-y-provider server uses @hocuspocus to
do collaborative editing on docs.
2024-09-04 21:10:24 +02:00
Anthony LC 1139c0abea ♻️(frontend) replace y-webrtc by @hocuspocus
y-webrtc had some issues, users had difficulties
to connect with each others.
We replace it by @hocuspocus/provider.
2024-09-04 21:10:24 +02:00
Anthony LC 9e1979f637 🐛(docker) add emoji font
In order to have the emoji font available in
the container, we need to install it.
The font will be then available in the
PDF export.
2024-09-03 17:37:56 +02:00
Anthony LC ddd93ab0c5 🐛(frontend) close panel when unmount
When the panel is unmounted, the summary and
version panel should be closed.
2024-09-03 17:37:56 +02:00
Anthony LC 85044fd665 (frontend) summary feature
Add the summary feature to the doc.
We will be able to access part of the doc quickly
from the summary.
2024-09-03 15:55:25 +02:00
Anthony LC b83875fc97 ♻️(frontend) move Panel component
We will have multiple Panel components in the future,
so we move it to the root of the components folder.
We refacto the Version Panel to use the new
Panel component.
2024-09-03 15:55:25 +02:00
Anthony LC 7a8caf5475 🐛(backend) compatibility issue with django and easy_thumbnails
There is a compatibility issue between django 5.1
and easy_thumbnails 2.9.
This commit fixes the issue.
2024-09-03 11:36:50 +02:00
renovate[bot] e927f2c004 ⬆️(dependencies) update python dependencies 2024-09-03 11:36:50 +02:00
Anthony LC 7f25b05474 ⚗️(frontend) add button Restore this version near title
When a user is on a page version, we will display
a button "Restore this version" near the title of
the page. It gives an obvious way to restore the
version of the doc.
2024-09-02 17:02:23 +02:00
Anthony LC 296b5dbf59 ♻️(frontend) add modal confirmation restore version
Add modal confirmation restore version explaining
that the current version will be replaced
by the selected version, and that some data
may be lost.
2024-09-02 17:02:23 +02:00
Anthony LC accbda44e2 ♻️(frontend) open version panel from docs options
Versions panel is a feature that will not be used
by all users, so it should be hidden
by default. The user can open it from the docs
options.
2024-09-02 17:02:23 +02:00
Anthony LC f2a78ada47 🔧(helm) replace storage url in ingressMedia
There is no mechanism to have the media storage
URL from a secret from the ingress.
The media storage URL has to be hardcoded.
We replace the media storage URL in the ingress,
if we change the cluster, we will have to update
these urls.
2024-09-02 12:17:40 +02:00
renovate[bot] 4cb0423511 ⬆️(dependencies) update js dependencies 2024-09-02 09:50:21 +02:00
Anthony LC 766aee6a92 💄(frontend) code background darkened on editor
The "code" was not visible on the editor
because the background was too light.
The background color was darkened to make the
"code" more visible.
2024-08-30 15:58:25 +02:00
Anthony LC 3d19893091 🔥(frontend) remove saving modal
The saving toast are removed.
Users were complaining about the toast
that was shown when saving a document.
2024-08-30 15:43:48 +02:00
Anthony LC 00b223f648 🔥(frontend) hide markdown button if not text
If we are selected a block that is not a text block,
we hide the markdown button.
2024-08-30 15:43:48 +02:00
Anthony LC 38b32c1227 ️(tilt) stop kind-registry to restart when stopped
To save resources, it is nive to be able to stop
the kind-registry when it is not needed.
This commit allow us to stop it.
2024-08-29 18:31:26 +02:00
Anthony LC 1ff3d9c54e 🧑‍💻(ngnix) add conf ngnix to proxy media url
In development mode with docker-compose, we need to
configure Nginx to proxy requests to the Minio server.
Before to proxy to Minio, we need to
authenticate the request, so we proxy to the
Django server first to fill the request with the
necessary headers, then we proxy to Minio.
2024-08-29 18:31:26 +02:00
Anthony LC 6eff21f51e (frontend) add upload to the doc editor
We can now upload images to the doc editor.
The image is uploaded to the server
and the URL is inserted into the editor.
2024-08-29 18:31:26 +02:00
Anthony LC 3eb8f88b5c 👔(frontend) integrate attachment-upload endpoint
Integrate the `documents/${docId}/attachment-upload/`
endpoint. This endpoint is used to upload attachments
to a document.
To have automatically the good content-type form-data,
the `fetchApi` function has been updated to remove the
prefill `Content-Type` header.
2024-08-29 18:31:26 +02:00
renovate[bot] 3a3483b776 ⬆️(dependencies) update js dependencies 2024-08-29 14:11:11 +02:00
Samuel Paccoud - DINUM 67a20f249e (backend) add url to download media attachments with access rights
We make use of nginx subrequests to block media file downloads while
we check for access rights. The request is then proxied to the object
storage engine and authorization is added via the "Authorization"
header. This way the media urls are static and can be stored in the
document's json content without compromising on security: access
control is done on all requests based on the user cookie session.
2024-08-27 15:59:44 +02:00
Samuel Paccoud - DINUM c9f1356d3e (backend) allow uploading images as attachments to a document
We only rely on S3 to store attachments for a document. Nothing
is persisted in the database as the image media urls will be
stored in the document json.
2024-08-27 15:59:44 +02:00
Samuel Paccoud - DINUM f12708acee ⬆️(backend) upgrade boto3 to 1.14.4 for unsigned urls
For media urls, we want to compute authorization as a header
instead of computing signed urls.

The url of a media file can then be computed without the
querystring authorization part. This requires upgrading
django-storages to the 1.14 version to benefit from the
"unsigned connection" in the S3Storage backend.
2024-08-27 15:59:44 +02:00
Anthony LC 58eaea000c 🔖(patch) release 1.2.1
Changed:
- ♻️ Change ordering docs datagrid
- 🔥(helm) use scaleway email
2024-08-23 16:27:52 +02:00
Anthony LC 7d97a037f6 🧑‍💻(makefile) bump-packages-version
Create the command bump-packages-version in Makefile.
This command will bump the version of
all the javascript packages in the project.
2024-08-23 16:27:52 +02:00
Anthony LC c830b4dae6 ♻️(email) replace base64 image with a link
The emails were too big, gmail by example was not
able to display them correctly.
It was caused by base64 image, so they are
replaced with a link to the image.

We fixed the link to the website, it will improve
the score of the email.
2024-08-23 15:37:01 +02:00
Anthony LC a0fe98e156 🔥(helm) configure staging to use scaleway email
Change staging configuration to use scaleway transactionnal email
2024-08-23 15:37:01 +02:00
Anthony LC fa105e5b54 📌(e2e) pin pdf-parse to 1.1.1
pdf-parse was not pinned to a specific version.
This could lead to unexpected behavior
if the package is updated.
This change pins pdf-parse to version 1.1.1.
2024-08-23 14:29:52 +02:00
Anthony LC ced850aecf ♻️(frontend) datagrid ordered by updated_at desc
The datagrid is now ordered by updated_at desc.
2024-08-23 14:29:52 +02:00
Anthony LC 3a420c0416 ♻️(backend) document list order by updated_at desc
Document list is now ordered by updated_at in
descending order.
Test cases were improved as well.
2024-08-23 14:29:52 +02:00
162 changed files with 6483 additions and 4889 deletions
+3 -3
View File
@@ -104,7 +104,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-and-push-y-webrtc-signaling:
build-and-push-y-provider:
runs-on: ubuntu-latest
steps:
-
@@ -132,7 +132,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: lasuite/impress-y-webrtc-signaling
images: lasuite/impress-y-provider
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
@@ -143,7 +143,7 @@ jobs:
with:
context: .
file: ./src/frontend/Dockerfile
target: y-webrtc-signaling
target: y-provider
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
+10 -2
View File
@@ -139,7 +139,7 @@ jobs:
with:
targets: |
app-dev
y-webrtc-signaling
y-provider
load: true
set: |
*.cache-from=type=gha,scope=cached-stage
@@ -149,6 +149,10 @@ jobs:
run: |
make run
- name: Start Nginx for the frontend
run: |
docker compose up --force-recreate -d nginx-front
- name: Apply DRF migrations
run: |
make migrate
@@ -213,7 +217,7 @@ jobs:
with:
targets: |
app-dev
y-webrtc-signaling
y-provider
load: true
set: |
*.cache-from=type=gha,scope=cached-stage
@@ -223,6 +227,10 @@ jobs:
run: |
make run
- name: Start Nginx for the frontend
run: |
docker compose up --force-recreate -d nginx-front
- name: Apply DRF migrations
run: |
make migrate
+48 -2
View File
@@ -6,9 +6,53 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.2.0] - 2024-08-06
## Added
- ✨(frontend) add copy link button #235
- 🛂(frontend) access public docs without being logged #235
## Changed
- 🚚(frontend) change visibility in share modal #235
## [1.3.0] - 2024-09-05
## Added
- ✨Add image attachments with access control
- ✨(frontend) Upload image to a document #211
- ✨(frontend) Summary #223
- ✨(frontend) update meta title for docs page #231
## Changed
- 💄(frontend) code background darkened on editor #214
- 🔥(frontend) hide markdown button if not text #213
## Fixed
- 🐛 Fix emoticon in pdf export #225
- 🐛 Fix collaboration on document #226
- 🐛 (docker) Fix compatibility with mac #230
## Removed
- 🔥(frontend) remove saving modal #213
## [1.2.1] - 2024-08-23
## Changed
- ♻️ Change ordering docs datagrid #195
- 🔥(helm) use scaleway email #194
## [1.2.0] - 2024-08-22
## Added
@@ -97,7 +141,9 @@ and this project adheres to
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/numerique-gouv/impress/compare/v1.2.0...main
[unreleased]: https://github.com/numerique-gouv/impress/compare/v1.3.0...main
[1.3.0]: https://github.com/numerique-gouv/impress/releases/v1.3.0
[1.2.1]: https://github.com/numerique-gouv/impress/releases/v1.2.1
[1.2.0]: https://github.com/numerique-gouv/impress/releases/v1.2.0
[1.1.0]: https://github.com/numerique-gouv/impress/releases/v1.1.0
[1.0.0]: https://github.com/numerique-gouv/impress/releases/v1.0.0
+1
View File
@@ -76,6 +76,7 @@ RUN apt-get update && \
libpango-1.0-0 \
libpangocairo-1.0-0 \
pandoc \
fonts-noto-color-emoji \
shared-mime-info && \
rm -rf /var/lib/apt/lists/*
+11 -1
View File
@@ -104,7 +104,7 @@ logs: ## display app-dev logs (follow mode)
run: ## start the wsgi (production) and development server
@$(COMPOSE) up --force-recreate -d celery-dev
@$(COMPOSE) up --force-recreate -d y-webrtc-signaling
@$(COMPOSE) up --force-recreate -d y-provider
@echo "Wait for postgresql to be up..."
@$(WAIT_DB)
.PHONY: run
@@ -313,3 +313,13 @@ start-tilt: ## start the kubernetes cluster using kind
tilt up -f ./bin/Tiltfile
.PHONY: build-k8s-cluster
VERSION_TYPE ?= minor
bump-packages-version: ## bump the version of the project - VERSION_TYPE can be "major", "minor", "patch"
cd ./src/mail && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/apps/e2e/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/apps/impress/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/servers/y-provider/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/packages/eslint-config-impress/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/packages/i18n/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
.PHONY: bump-packages-version
+3 -3
View File
@@ -18,13 +18,13 @@ docker_build(
)
docker_build(
'localhost:5001/impress-y-webrtc-signaling:latest',
'localhost:5001/impress-y-provider:latest',
context='..',
dockerfile='../src/frontend/Dockerfile',
only=['./src/frontend/', './docker/', './.dockerignore'],
target = 'y-webrtc-signaling',
target = 'y-provider',
live_update=[
sync('../src/frontend/apps/y-webrtc-signaling/src', '/home/frontend/apps/y-webrtc-signaling/src'),
sync('../src/frontend/servers/y-provider/src', '/home/frontend/servers/y-provider/src'),
]
)
+1 -1
View File
@@ -16,7 +16,7 @@ reg_name='kind-registry'
reg_port='5001'
if [ "$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" != 'true' ]; then
docker run \
-d --restart=always -p "127.0.0.1:${reg_port}:5000" --network bridge --name "${reg_name}" \
-d --restart=unless-stopped -p "127.0.0.1:${reg_port}:5000" --network bridge --name "${reg_name}" \
registry:2
fi
+14 -7
View File
@@ -121,6 +121,14 @@ services:
depends_on:
- keycloak
nginx-front:
image: nginx:1.25
ports:
- "3000:3000"
volumes:
- ./src/frontend/apps/impress/conf/default.conf:/etc/nginx/conf.d/default.conf
- ./src/frontend/apps/impress/out:/usr/share/nginx/html
dockerize:
image: jwilder/dockerize
@@ -141,19 +149,19 @@ services:
volumes:
- ".:/app"
y-webrtc-signaling:
y-provider:
user: ${DOCKER_USER:-1000}
build:
context: .
dockerfile: ./src/frontend/Dockerfile
target: y-webrtc-signaling
target: y-provider
restart: unless-stopped
ports:
- "4444:4444"
volumes:
- ./src/frontend/apps/y-webrtc-signaling:/home/frontend/apps/y-webrtc-signaling
- /home/frontend/apps/y-webrtc-signaling/node_modules/
- /home/frontend/apps/y-webrtc-signaling/dist/
- ./src/frontend/servers/y-provider:/home/frontend/servers/y-provider
- /home/frontend/servers/y-provider/node_modules/
- /home/frontend/servers/y-provider/dist/
frontend-dev:
user: "${DOCKER_USER:-1000}"
@@ -167,12 +175,11 @@ services:
- ./src/frontend/apps/impress:/home/frontend/apps/impress
- /home/frontend/node_modules/
depends_on:
- y-webrtc-signaling
- y-provider
- celery-dev
kc_postgresql:
image: postgres:14.3
platform: linux/amd64
ports:
- "5433:5432"
env_file:
@@ -4,6 +4,36 @@ server {
server_name localhost;
charset utf-8;
location /media/ {
# Auth request configuration
auth_request /auth;
auth_request_set $authHeader $upstream_http_authorization;
auth_request_set $authDate $upstream_http_x_amz_date;
auth_request_set $authContentSha256 $upstream_http_x_amz_content_sha256;
# Pass specific headers from the auth response
proxy_set_header Authorization $authHeader;
proxy_set_header X-Amz-Date $authDate;
proxy_set_header X-Amz-Content-SHA256 $authContentSha256;
# Get resource from Minio
proxy_pass http://minio:9000/impress-media-storage/;
proxy_set_header Host minio:9000;
}
location /auth {
proxy_pass http://app-dev:8000/api/v1.0/documents/retrieve-auth/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Original-URL $request_uri;
# Prevent the body from being passed
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-Method $request_method;
}
location / {
proxy_pass http://keycloak:8080;
proxy_set_header Host $host;
+2 -2
View File
@@ -23,9 +23,9 @@ Whenever we are cooking a new release (e.g. `4.18.1`) we should follow a standar
pullPolicy: Always
tag: "v4.18.1"
webrtc:
y-provider:
image:
repository: lasuite/impress-y-webrtc-signaling
repository: lasuite/impress-y-provider
pullPolicy: Always
tag: "v4.18.1"
```
+1 -7
View File
@@ -13,13 +13,7 @@
"enabled": false,
"groupName": "ignored js dependencies",
"matchManagers": ["npm"],
"matchPackageNames": [
"fetch-mock",
"node",
"node-fetch",
"i18next-parser",
"eslint"
]
"matchPackageNames": ["fetch-mock", "node", "node-fetch", "eslint"]
}
]
}
+1 -1
Submodule secrets updated: 1485c6dc9d...2643697e5f
+31
View File
@@ -1,5 +1,8 @@
"""Client serializers for the impress core app."""
import mimetypes
from django.conf import settings
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
@@ -153,6 +156,34 @@ class DocumentSerializer(BaseResourceSerializer):
read_only_fields = ["id", "accesses", "abilities", "created_at", "updated_at"]
# Suppress the warning about not implementing `create` and `update` methods
# since we don't use a model and only rely on the serializer for validation
# pylint: disable=abstract-method
class FileUploadSerializer(serializers.Serializer):
"""Receive file upload requests."""
file = serializers.FileField()
def validate_file(self, file):
"""Add file size and type constraints as defined in settings."""
# Validate file size
if file.size > settings.DOCUMENT_IMAGE_MAX_SIZE:
max_size = settings.DOCUMENT_IMAGE_MAX_SIZE // (1024 * 1024)
raise serializers.ValidationError(
f"File size exceeds the maximum limit of {max_size:d} MB."
)
# Validate file type
mime_type, _ = mimetypes.guess_type(file.name)
if mime_type not in settings.DOCUMENT_IMAGE_ALLOWED_MIME_TYPES:
mime_types = ", ".join(settings.DOCUMENT_IMAGE_ALLOWED_MIME_TYPES)
raise serializers.ValidationError(
f"File type '{mime_type:s}' is not allowed. Allowed types are: {mime_types:s}"
)
return file
class TemplateSerializer(BaseResourceSerializer):
"""Serialize templates."""
+33
View File
@@ -0,0 +1,33 @@
"""Util to generate S3 authorization headers for object storage access control"""
from django.core.files.storage import default_storage
import botocore
def generate_s3_authorization_headers(key):
"""
Generate authorization headers for an s3 object.
These headers can be used as an alternative to signed urls with many benefits:
- the urls of our files never expire and can be stored in our documents' content
- we don't leak authorized urls that could be shared (file access can only be done
with cookies)
- access control is truly realtime
- the object storage service does not need to be exposed on internet
"""
url = default_storage.unsigned_connection.meta.client.generate_presigned_url(
"get_object",
ExpiresIn=0,
Params={"Bucket": default_storage.bucket_name, "Key": key},
)
request = botocore.awsrequest.AWSRequest(method="get", url=url)
s3_client = default_storage.connection.meta.client
# pylint: disable=protected-access
credentials = s3_client._request_signer._credentials # noqa: SLF001
frozen_credentials = credentials.get_frozen_credentials()
region = s3_client.meta.region_name
auth = botocore.auth.S3SigV4Auth(frozen_credentials, "s3", region)
auth.add_auth(request)
return request
+89 -2
View File
@@ -1,6 +1,13 @@
"""API endpoints"""
import os
import re
import uuid
from urllib.parse import urlparse
from django.conf import settings
from django.contrib.postgres.aggregates import ArrayAgg
from django.core.files.storage import default_storage
from django.db.models import (
OuterRef,
Q,
@@ -25,10 +32,22 @@ from rest_framework import (
from core import models
from core.utils import email_invitation
from . import permissions, serializers
from . import permissions, serializers, utils
ATTACHMENTS_FOLDER = "attachments"
UUID_REGEX = (
r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"
)
FILE_EXT_REGEX = r"\.[a-zA-Z]{3,4}"
MEDIA_URL_PATTERN = re.compile(
f"{settings.MEDIA_URL:s}({UUID_REGEX:s})/"
f"({ATTACHMENTS_FOLDER:s}/{UUID_REGEX:s}{FILE_EXT_REGEX:s})$"
)
# pylint: disable=too-many-ancestors
ATTACHMENTS_FOLDER = "attachments"
class NestedGenericViewSet(viewsets.GenericViewSet):
"""
@@ -265,7 +284,7 @@ class ResourceAccessViewsetMixin:
):
return drf_response.Response(
{"detail": "Cannot delete the last owner access for the resource."},
status=403,
status=status.HTTP_403_FORBIDDEN,
)
return super().destroy(request, *args, **kwargs)
@@ -310,6 +329,7 @@ class DocumentViewSet(
access_model_class = models.DocumentAccess
resource_field_name = "document"
queryset = models.Document.objects.all()
ordering = ["-updated_at"]
def perform_create(self, serializer):
"""
@@ -389,6 +409,73 @@ class DocumentViewSet(
}
)
@decorators.action(detail=True, methods=["post"], url_path="attachment-upload")
def attachment_upload(self, request, *args, **kwargs):
"""Upload a file related to a given document"""
# Check permissions first
document = self.get_object()
# Validate metadata in payload
serializer = serializers.FileUploadSerializer(data=request.data)
if not serializer.is_valid():
return drf_response.Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
# Extract the file extension from the original filename
file = serializer.validated_data["file"]
extension = os.path.splitext(file.name)[1]
# Generate a generic yet unique filename to store the image in object storage
file_id = uuid.uuid4()
key = f"{document.key_base}/{ATTACHMENTS_FOLDER:s}/{file_id!s}{extension:s}"
default_storage.save(key, file)
return drf_response.Response(
{"file": f"{settings.MEDIA_URL:s}{key:s}"}, status=status.HTTP_201_CREATED
)
@decorators.action(detail=False, methods=["get"], url_path="retrieve-auth")
def retrieve_auth(self, request, *args, **kwargs):
"""
This view is used by an Nginx subrequest to control access to a document's
attachment file.
The original url is passed by nginx in the "HTTP_X_ORIGINAL_URL" header.
See corresponding ingress configuration in Helm chart and read about the
nginx.ingress.kubernetes.io/auth-url annotation to understand how the Nginx ingress
is configured to do this.
Based on the original url and the logged in user, we must decide if we authorize Nginx
to let this request go through (by returning a 200 code) or if we block it (by returning
a 403 error). Note that we return 403 errors without any further details for security
reasons.
When we let the request go through, we compute authorization headers that will be added to
the request going through thanks to the nginx.ingress.kubernetes.io/auth-response-headers
annotation. The request will then be proxied to the object storage backend who will
respond with the file after checking the signature included in headers.
"""
original_url = urlparse(request.META.get("HTTP_X_ORIGINAL_URL"))
match = MEDIA_URL_PATTERN.search(original_url.path)
try:
pk, attachment_key = match.groups()
except AttributeError as excpt:
raise exceptions.PermissionDenied() from excpt
# Check permission
try:
document = models.Document.objects.get(pk=pk)
except models.Document.DoesNotExist as excpt:
raise exceptions.PermissionDenied() from excpt
if not document.get_abilities(request.user).get("retrieve", False):
raise exceptions.PermissionDenied()
# Generate authorization headers and return an authorization to proceed with the request
request = utils.generate_s3_authorization_headers(f"{pk:s}/{attachment_key:s}")
return drf_response.Response("authorized", headers=request.headers, status=200)
class DocumentAccessViewSet(
ResourceAccessViewsetMixin,
+32 -23
View File
@@ -36,18 +36,19 @@ logger = getLogger(__name__)
def get_resource_roles(resource, user):
"""Compute the roles a user has on a resource."""
roles = []
if user.is_authenticated:
if not user.is_authenticated:
return []
try:
roles = resource.user_roles or []
except AttributeError:
teams = user.get_teams()
try:
roles = resource.user_roles or []
except AttributeError:
teams = user.get_teams()
try:
roles = resource.accesses.filter(
models.Q(user=user) | models.Q(team__in=teams),
).values_list("role", flat=True)
except (models.ObjectDoesNotExist, IndexError):
roles = []
roles = resource.accesses.filter(
models.Q(user=user) | models.Q(team__in=teams),
).values_list("role", flat=True)
except (models.ObjectDoesNotExist, IndexError):
roles = []
return roles
@@ -324,16 +325,23 @@ class Document(BaseModel):
file_key = self.file_key
bytes_content = self._content.encode("utf-8")
if default_storage.exists(file_key):
# Attempt to directly check if the object exists using the storage client.
try:
response = default_storage.connection.meta.client.head_object(
Bucket=default_storage.bucket_name, Key=file_key
)
except ClientError as excpt:
# If the error is a 404, the object doesn't exist, so we should create it.
if excpt.response["Error"]["Code"] == "404":
has_changed = True
else:
raise
else:
# Compare the existing ETag with the MD5 hash of the new content.
has_changed = (
response["ETag"].strip('"')
!= hashlib.md5(bytes_content).hexdigest() # noqa
!= hashlib.md5(bytes_content).hexdigest() # noqa: S324
)
else:
has_changed = True
if has_changed:
content_file = ContentFile(bytes_content)
@@ -396,7 +404,7 @@ class Document(BaseModel):
response = default_storage.connection.meta.client.list_object_versions(
Bucket=default_storage.bucket_name,
Prefix=self.file_key,
MaxKeys=settings.S3_VERSIONS_PAGE_SIZE,
MaxKeys=settings.DOCUMENT_VERSIONS_PAGE_SIZE,
**token,
)
@@ -414,7 +422,7 @@ class Document(BaseModel):
if response["NextVersionIdMarker"]:
return self.get_versions_slice(
from_version_id=response["NextVersionIdMarker"],
page_size=settings.S3_VERSIONS_PAGE_SIZE,
page_size=settings.DOCUMENT_VERSIONS_PAGE_SIZE,
from_datetime=from_datetime,
)
return {
@@ -426,9 +434,9 @@ class Document(BaseModel):
response = default_storage.connection.meta.client.list_object_versions(
Bucket=default_storage.bucket_name,
Prefix=self.file_key,
MaxKeys=min(page_size, settings.S3_VERSIONS_PAGE_SIZE)
MaxKeys=min(page_size, settings.DOCUMENT_VERSIONS_PAGE_SIZE)
if page_size
else settings.S3_VERSIONS_PAGE_SIZE,
else settings.DOCUMENT_VERSIONS_PAGE_SIZE,
**token,
)
return {
@@ -468,13 +476,14 @@ class Document(BaseModel):
return {
"destroy": RoleChoices.OWNER in roles,
"attachment_upload": is_owner_or_admin or is_editor,
"manage_accesses": is_owner_or_admin,
"partial_update": is_owner_or_admin or is_editor,
"retrieve": can_get,
"update": is_owner_or_admin or is_editor,
"versions_destroy": is_owner_or_admin,
"versions_list": can_get_versions,
"versions_retrieve": can_get_versions,
"manage_accesses": is_owner_or_admin,
"update": is_owner_or_admin or is_editor,
"partial_update": is_owner_or_admin or is_editor,
"retrieve": can_get,
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

@@ -0,0 +1,197 @@
"""
Test file uploads API endpoint for users in impress's core app.
"""
import re
import uuid
from django.core.files.base import ContentFile
from django.core.files.uploadedfile import SimpleUploadedFile
import pytest
from rest_framework.test import APIClient
from core import factories
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
def test_api_documents_attachment_upload_anonymous():
"""Anonymous users can't upload attachments to a document."""
document = factories.DocumentFactory()
file = SimpleUploadedFile("test_file.jpg", b"Dummy content")
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
response = APIClient().post(url, {"file": file}, format="multipart")
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
def test_api_documents_attachment_upload_authenticated_public():
"""
Users who are not related to a public document should not be allowed to upload an attachment.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=True)
file = SimpleUploadedFile("test_file.jpg", b"Dummy content")
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
response = client.post(url, {"file": file}, format="multipart")
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
def test_api_documents_attachment_upload_authenticated_private():
"""
Users who are not related to a private document should not be able to upload an attachment.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=False)
file = SimpleUploadedFile("test_file.jpg", b"Dummy content")
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
response = client.post(url, {"file": file}, format="multipart")
assert response.status_code == 404
assert response.json() == {"detail": "No Document matches the given query."}
@pytest.mark.parametrize("via", VIA)
def test_api_documents_attachment_upload_reader(via, mock_user_get_teams):
"""
Users who are simple readers on a document should not be allowed to upload an attachment.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="reader")
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="reader"
)
file = SimpleUploadedFile("test_file.jpg", b"Dummy content")
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
response = client.post(url, {"file": file}, format="multipart")
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
@pytest.mark.parametrize("via", VIA)
def test_api_documents_attachment_upload_success(via, role, mock_user_get_teams):
"""
Editors, administrators and owners of a document should be able to upload an attachment.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)
file = SimpleUploadedFile("test_file.jpg", b"Dummy content")
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
response = client.post(url, {"file": file}, format="multipart")
assert response.status_code == 201
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.jpg")
match = pattern.search(response.json()["file"])
file_id = match.group(1)
# Validate that file_id is a valid UUID
uuid.UUID(file_id)
def test_api_documents_attachment_upload_invalid(client):
"""Attempt to upload without a file should return an explicit error."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(users=[(user, "owner")])
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
response = client.post(url, {}, format="multipart")
assert response.status_code == 400
assert response.json() == {"file": ["No file was submitted."]}
def test_api_documents_attachment_upload_size_limit_exceeded(settings):
"""The uploaded file should not exceeed the maximum size in settings."""
settings.DOCUMENT_IMAGE_MAX_SIZE = 1048576 # 1 MB for test
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(users=[(user, "owner")])
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
# Create a temporary file larger than the allowed size
content = b"a" * (1048576 + 1)
file = ContentFile(content, name="test.jpg")
response = client.post(url, {"file": file}, format="multipart")
assert response.status_code == 400
assert response.json() == {"file": ["File size exceeds the maximum limit of 1 MB."]}
def test_api_documents_attachment_upload_type_not_allowed(settings):
"""The uploaded file should be of a whitelisted type."""
settings.DOCUMENT_IMAGE_ALLOWED_MIME_TYPES = ["image/jpeg", "image/png"]
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(users=[(user, "owner")])
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
# Create a temporary file with a not allowed type (e.g., text file)
file = ContentFile(b"a" * 1048576, name="test.txt")
response = client.post(url, {"file": file}, format="multipart")
assert response.status_code == 400
assert response.json() == {
"file": [
"File type 'text/plain' is not allowed. Allowed types are: image/jpeg, image/png"
]
}
@@ -169,87 +169,26 @@ def test_api_documents_list_authenticated_distinct():
assert content["results"][0]["id"] == str(document.id)
def test_api_documents_order_created_at_desc():
def test_api_documents_order_updated_at_desc_default():
"""
Test that the endpoint GET documents is sorted in 'created_at' descending order by default.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
documents_created = [
document.created_at.isoformat().replace("+00:00", "Z")
for document in factories.DocumentFactory.create_batch(5, is_public=True)
]
documents_created.sort(reverse=True)
response = client.get(
"/api/v1.0/documents/",
)
assert response.status_code == 200
response_data = response.json()
response_document_created = [
document["created_at"] for document in response_data["results"]
]
assert (
response_document_created == documents_created
), "created_at values are not sorted from newest to oldest"
def test_api_documents_order_created_at_asc():
"""
Test that the 'created_at' field is sorted in ascending order
when the 'ordering' query parameter is set.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
documents_created = [
document.created_at.isoformat().replace("+00:00", "Z")
for document in factories.DocumentFactory.create_batch(5, is_public=True)
]
documents_created.sort()
response = client.get(
"/api/v1.0/documents/?ordering=created_at",
)
assert response.status_code == 200
response_data = response.json()
response_document_created = [
document["created_at"] for document in response_data["results"]
]
assert (
response_document_created == documents_created
), "created_at values are not sorted from oldest to newest"
def test_api_documents_order_updated_at_desc():
"""
Test that the 'updated_at' field is sorted in descending order
when the 'ordering' query parameter is set.
Test that the endpoint GET documents is sorted in 'updated_at' descending order by default.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
# Updated at next year to ensure the order is correct
documents_updated = [
document.updated_at.isoformat().replace("+00:00", "Z")
for document in factories.DocumentFactory.create_batch(5, is_public=True)
for document in factories.DocumentFactory.create_batch(
5, is_public=True, updated_at=fake.date_time_this_year(before_now=False)
)
]
documents_updated.sort(reverse=True)
response = APIClient().get(
"/api/v1.0/documents/?ordering=-updated_at",
"/api/v1.0/documents/",
)
assert response.status_code == 200
@@ -264,97 +203,99 @@ def test_api_documents_order_updated_at_desc():
), "updated_at values are not sorted from newest to oldest"
def test_api_documents_order_updated_at_asc():
@pytest.mark.parametrize(
"ordering_field, factory_field",
[
("-created_at", "created_at"),
("-updated_at", "updated_at"),
("-title", "title"),
],
)
def test_api_documents_ordering_desc(ordering_field, factory_field):
"""
Test that the 'updated_at' field is sorted in ascending order
Test that the specified field is sorted in descending order
when the 'ordering' query parameter is set.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
documents_updated = [
document.updated_at.isoformat().replace("+00:00", "Z")
for document in factories.DocumentFactory.create_batch(5, is_public=True)
]
if factory_field == "title":
documents_field_values = [
factories.DocumentFactory(
is_public=True, title=fake.sentence(nb_words=4)
).title
for _ in range(5)
]
else:
documents_field_values = [
getattr(document, factory_field).isoformat().replace("+00:00", "Z")
for document in factories.DocumentFactory.create_batch(5, is_public=True)
]
documents_updated.sort()
documents_field_values.sort(reverse=True)
response = APIClient().get(
"/api/v1.0/documents/?ordering=updated_at",
response = client.get(
f"/api/v1.0/documents/?ordering={ordering_field}"
if ordering_field != "-created_at"
else "/api/v1.0/documents/",
)
assert response.status_code == 200
response_data = response.json()
response_document_updated = [
document["updated_at"] for document in response_data["results"]
response_documents_field_values = [
document[factory_field] for document in response_data["results"]
]
assert (
response_document_updated == documents_updated
), "updated_at values are not sorted from oldest to newest"
response_documents_field_values == documents_field_values
), f"{factory_field} values are not sorted as expected"
def test_api_documents_order_title_desc():
@pytest.mark.parametrize(
"field",
[
("updated_at"),
("title"),
("created_at"),
],
)
def test_api_documents_ordering_asc(field):
"""
Test that the 'title' field is sorted in descending order
Test that the specified field is sorted in ascending order
when the 'ordering' query parameter is set.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
documents_title = [
factories.DocumentFactory(is_public=True, title=fake.sentence(nb_words=4)).title
for _ in range(5)
]
if field == "title":
documents_field_values = [
factories.DocumentFactory(
is_public=True, title=fake.sentence(nb_words=4)
).title
for _ in range(5)
]
else:
documents_field_values = [
getattr(document, field).isoformat().replace("+00:00", "Z")
for document in factories.DocumentFactory.create_batch(5, is_public=True)
]
documents_title.sort(reverse=True)
documents_field_values.sort()
response = APIClient().get(
"/api/v1.0/documents/?ordering=-title",
response = client.get(
f"/api/v1.0/documents/?ordering={field}",
)
assert response.status_code == 200
response_data = response.json()
response_documents_title = [
document["title"] for document in response_data["results"]
response_documents_field_values = [
document[field] for document in response_data["results"]
]
assert (
response_documents_title == documents_title
), "title values are not sorted descending"
def test_api_documents_order_title_asc():
"""
Test that the 'title' field is sorted in ascending order
when the 'ordering' query parameter is set.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
documents_title = [
factories.DocumentFactory(is_public=True, title=fake.sentence(nb_words=4)).title
for _ in range(5)
]
documents_title.sort()
response = APIClient().get(
"/api/v1.0/documents/?ordering=title",
)
assert response.status_code == 200
response_data = response.json()
response_documents_title = [
document["title"] for document in response_data["results"]
]
assert (
response_documents_title == documents_title
), "title values are not sorted ascending"
response_documents_field_values == documents_field_values
), f"{field} values are not sorted as expected"
@@ -22,6 +22,7 @@ def test_api_documents_retrieve_anonymous_public():
"id": str(document.id),
"abilities": {
"destroy": False,
"attachment_upload": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": True,
@@ -69,6 +70,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public():
"id": str(document.id),
"abilities": {
"destroy": False,
"attachment_upload": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": True,
@@ -0,0 +1,213 @@
"""
Test file uploads API endpoint for users in impress's core app.
"""
import uuid
from io import BytesIO
from urllib.parse import urlparse
from django.conf import settings
from django.core.files.storage import default_storage
from django.utils import timezone
import pytest
import requests
from rest_framework.test import APIClient
from core import factories
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
def test_api_documents_retrieve_auth_anonymous_public():
"""Anonymous users should be able to retrieve attachments linked to a public document"""
document = factories.DocumentFactory(is_public=True)
filename = f"{uuid.uuid4()!s}.jpg"
key = f"{document.pk!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",
)
original_url = f"http://localhost/media/{key:s}"
response = APIClient().get(
"/api/v1.0/documents/retrieve-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"
def test_api_documents_retrieve_auth_anonymous_not_public():
"""
Anonymous users should not be allowed to retrieve attachments linked to a document
that is not public.
"""
document = factories.DocumentFactory(is_public=False)
filename = f"{uuid.uuid4()!s}.jpg"
media_url = f"http://localhost/media/{document.pk!s}/attachments/{filename:s}"
response = APIClient().get(
"/api/v1.0/documents/retrieve-auth/", HTTP_X_ORIGINAL_URL=media_url
)
assert response.status_code == 403
assert "Authorization" not in response
def test_api_documents_retrieve_auth_authenticated_public():
"""
Authenticated users who are not related to a document should be able to
retrieve attachments linked to a public document.
"""
document = factories.DocumentFactory(is_public=True)
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
filename = f"{uuid.uuid4()!s}.jpg"
key = f"{document.pk!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",
)
original_url = f"http://localhost/media/{key:s}"
response = APIClient().get(
"/api/v1.0/documents/retrieve-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"
def test_api_documents_retrieve_auth_authenticated_not_public():
"""
Authenticated users who are not related to a document should not be allowed to
retrieve attachments linked to a document that is not public.
"""
document = factories.DocumentFactory(is_public=False)
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
filename = f"{uuid.uuid4()!s}.jpg"
media_url = f"http://localhost/media/{document.pk!s}/attachments/{filename:s}"
response = APIClient().get(
"/api/v1.0/documents/retrieve-auth/", HTTP_X_ORIGINAL_URL=media_url
)
assert response.status_code == 403
assert "Authorization" not in response
@pytest.mark.parametrize("is_public", [True, False])
@pytest.mark.parametrize("via", VIA)
def test_api_documents_retrieve_auth_related(via, is_public, mock_user_get_teams):
"""
Users who have a role on a document, whatever the role, should be able to
retrieve related attachments.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(is_public=is_public)
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user)
elif via == TEAM:
mock_user_get_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(document=document, team="lasuite")
filename = f"{uuid.uuid4()!s}.jpg"
key = f"{document.pk!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",
)
original_url = f"http://localhost/media/{key:s}"
response = client.get(
"/api/v1.0/documents/retrieve-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"
+28 -19
View File
@@ -63,10 +63,11 @@ def test_models_documents_get_abilities_anonymous_public():
abilities = document.get_abilities(AnonymousUser())
assert abilities == {
"destroy": False,
"retrieve": True,
"update": False,
"attachment_upload": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": True,
"update": False,
"versions_destroy": False,
"versions_list": False,
"versions_retrieve": False,
@@ -79,10 +80,11 @@ def test_models_documents_get_abilities_anonymous_not_public():
abilities = document.get_abilities(AnonymousUser())
assert abilities == {
"destroy": False,
"retrieve": False,
"update": False,
"attachment_upload": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": False,
"update": False,
"versions_destroy": False,
"versions_list": False,
"versions_retrieve": False,
@@ -95,10 +97,11 @@ def test_models_documents_get_abilities_authenticated_unrelated_public():
abilities = document.get_abilities(factories.UserFactory())
assert abilities == {
"destroy": False,
"retrieve": True,
"update": False,
"attachment_upload": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": True,
"update": False,
"versions_destroy": False,
"versions_list": False,
"versions_retrieve": False,
@@ -111,10 +114,11 @@ def test_models_documents_get_abilities_authenticated_unrelated_not_public():
abilities = document.get_abilities(factories.UserFactory())
assert abilities == {
"destroy": False,
"retrieve": False,
"update": False,
"attachment_upload": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": False,
"update": False,
"versions_destroy": False,
"versions_list": False,
"versions_retrieve": False,
@@ -128,10 +132,11 @@ def test_models_documents_get_abilities_owner():
abilities = access.document.get_abilities(access.user)
assert abilities == {
"destroy": True,
"retrieve": True,
"update": True,
"attachment_upload": True,
"manage_accesses": True,
"partial_update": True,
"retrieve": True,
"update": True,
"versions_destroy": True,
"versions_list": True,
"versions_retrieve": True,
@@ -144,10 +149,11 @@ def test_models_documents_get_abilities_administrator():
abilities = access.document.get_abilities(access.user)
assert abilities == {
"destroy": False,
"retrieve": True,
"update": True,
"attachment_upload": True,
"manage_accesses": True,
"partial_update": True,
"retrieve": True,
"update": True,
"versions_destroy": True,
"versions_list": True,
"versions_retrieve": True,
@@ -163,10 +169,11 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
assert abilities == {
"destroy": False,
"retrieve": True,
"update": True,
"attachment_upload": True,
"manage_accesses": False,
"partial_update": True,
"retrieve": True,
"update": True,
"versions_destroy": False,
"versions_list": True,
"versions_retrieve": True,
@@ -182,10 +189,11 @@ def test_models_documents_get_abilities_reader_user(django_assert_num_queries):
assert abilities == {
"destroy": False,
"retrieve": True,
"update": False,
"attachment_upload": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": True,
"update": False,
"versions_destroy": False,
"versions_list": True,
"versions_retrieve": True,
@@ -202,10 +210,11 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
assert abilities == {
"destroy": False,
"retrieve": True,
"update": False,
"attachment_upload": False,
"manage_accesses": False,
"partial_update": False,
"retrieve": True,
"update": False,
"versions_destroy": False,
"versions_list": True,
"versions_retrieve": True,
@@ -217,7 +226,7 @@ def test_models_documents_get_versions_slice(settings):
The "get_versions_slice" method should allow navigating all versions of
the document with pagination.
"""
settings.S3_VERSIONS_PAGE_SIZE = 4
settings.DOCUMENT_VERSIONS_PAGE_SIZE = 4
# Create a document with 7 versions
document = factories.DocumentFactory()
+19 -1
View File
@@ -138,7 +138,24 @@ class Base(Configuration):
environ_prefix=None,
)
S3_VERSIONS_PAGE_SIZE = 50
# Document images
DOCUMENT_IMAGE_MAX_SIZE = values.Value(
10 * (2**20), # 10MB
environ_name="DOCUMENT_IMAGE_MAX_SIZE",
environ_prefix=None,
)
DOCUMENT_IMAGE_ALLOWED_MIME_TYPES = [
"image/bmp",
"image/gif",
"image/jpeg",
"image/png",
"image/svg+xml",
"image/tiff",
"image/webp",
]
# Document versions
DOCUMENT_VERSIONS_PAGE_SIZE = 50
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
@@ -297,6 +314,7 @@ class Base(Configuration):
# Easy thumbnails
THUMBNAIL_EXTENSION = "webp"
THUMBNAIL_TRANSPARENCY_EXTENSION = "webp"
THUMBNAIL_DEFAULT_STORAGE_ALIAS = "default"
THUMBNAIL_ALIASES = {}
# Celery
+8 -8
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "1.2.0"
version = "1.3.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"boto3==1.35.0",
"boto3==1.35.10",
"Brotli==1.1.0",
"celery[redis]==5.4.0",
"django-configurations==2.5.1",
@@ -34,9 +34,9 @@ dependencies = [
"django-parler==2.3",
"redis==5.0.8",
"django-redis==5.4.0",
"django-storages[s3]==1.14.2",
"django-storages[s3]==1.14.4",
"django-timezone-field>=5.1",
"django==5.0.8",
"django==5.1",
"djangorestframework==3.15.2",
"drf_spectacular==0.27.2",
"dockerflow==2024.4.2",
@@ -70,17 +70,17 @@ dev = [
"django-extensions==3.2.3",
"drf-spectacular-sidecar==2024.7.1",
"ipdb==0.13.13",
"ipython==8.26.0",
"ipython==8.27.0",
"pyfakefs==5.6.0",
"pylint-django==2.5.5",
"pylint==3.2.6",
"pylint==3.2.7",
"pytest-cov==5.0.0",
"pytest-django==4.8.0",
"pytest-django==4.9.0",
"pytest==8.3.2",
"pytest-icdiff==0.9",
"pytest-xdist==3.6.1",
"responses==0.25.3",
"ruff==0.6.1",
"ruff==0.6.3",
"types-requests==2.32.0.20240712",
]
+7 -7
View File
@@ -1,10 +1,10 @@
FROM node:20-alpine as frontend-deps-y-webrtc-signaling
FROM node:20-alpine as frontend-deps-y-provider
WORKDIR /home/frontend/
COPY ./src/frontend/package.json ./package.json
COPY ./src/frontend/yarn.lock ./yarn.lock
COPY ./src/frontend/apps/y-webrtc-signaling/package.json ./apps/y-webrtc-signaling/package.json
COPY ./src/frontend/servers/y-provider/package.json ./servers/y-provider/package.json
COPY ./src/frontend/packages/eslint-config-impress/package.json ./packages/eslint-config-impress/package.json
RUN yarn install
@@ -14,10 +14,10 @@ COPY ./src/frontend/ .
# Copy entrypoint
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
# ---- y-webrtc-signaling ----
FROM frontend-deps-y-webrtc-signaling as y-webrtc-signaling
# ---- y-provider ----
FROM frontend-deps-y-provider as y-provider
WORKDIR /home/frontend/apps/y-webrtc-signaling
WORKDIR /home/frontend/servers/y-provider
RUN yarn build
# Un-privileged user running the application
@@ -64,8 +64,8 @@ WORKDIR /home/frontend/apps/impress
ARG FRONTEND_THEME
ENV NEXT_PUBLIC_THEME=${FRONTEND_THEME}
ARG SIGNALING_URL
ENV NEXT_PUBLIC_SIGNALING_URL=${SIGNALING_URL}
ARG Y_PROVIDER_URL
ENV NEXT_PUBLIC_Y_PROVIDER_URL=${Y_PROVIDER_URL}
ARG API_ORIGIN
ENV NEXT_PUBLIC_API_ORIGIN=${API_ORIGIN}

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

@@ -55,14 +55,25 @@ export const createDoc = async (
})
.fill(randomDocs[i]);
if (isPublic) {
await page.getByText('Is it public ?').click();
}
await expect(buttonCreate).toBeEnabled();
await buttonCreate.click();
await expect(page.locator('h2').getByText(randomDocs[i])).toBeVisible();
if (isPublic) {
await page.getByRole('button', { name: 'Share' }).click();
await page.getByText('Doc private').click();
await page.locator('.c__modal__backdrop').click({
position: { x: 0, y: 0 },
});
await expect(
page
.getByLabel('It is the card information about the document.')
.getByText('Public'),
).toBeVisible();
}
}
return randomDocs;
@@ -21,8 +21,6 @@ test.describe('Doc Create', () => {
).toBeVisible();
await expect(card.getByLabel('Document name')).toBeVisible();
await expect(card.getByText('Is it public ?')).toBeVisible();
await expect(
card.getByRole('button', {
name: 'Create the document',
@@ -46,13 +44,11 @@ test.describe('Doc Create', () => {
await expect(buttonCreateHomepage).toBeVisible();
});
test('create a new public doc', async ({ page, browserName }) => {
const [docTitle] = await createDoc(
page,
'My new doc',
browserName,
1,
true,
test('it creates a doc', async ({ page, browserName }) => {
const [docTitle] = await createDoc(page, 'My new doc', browserName, 1);
expect(await page.locator('title').textContent()).toMatch(
/My new doc - Docs/,
);
const header = page.locator('header').first();
@@ -65,11 +61,5 @@ test.describe('Doc Create', () => {
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
await expect(datagrid.getByText(docTitle)).toBeVisible();
const row = datagrid.getByRole('row').filter({
hasText: docTitle,
});
await expect(row.getByRole('cell').nth(0)).toHaveText('Public');
});
});
@@ -1,3 +1,5 @@
import path from 'path';
import { expect, test } from '@playwright/test';
import { createDoc, goToGridDoc, mockedDocument } from './common';
@@ -7,7 +9,7 @@ test.beforeEach(async ({ page }) => {
});
test.describe('Doc Editor', () => {
test('checks the Doc is connected to the webrtc server', async ({
test('checks the Doc is connected to the provider server', async ({
page,
browserName,
}) => {
@@ -27,12 +29,7 @@ test.describe('Doc Editor', () => {
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
const framesent = await framesentPromise;
const payload = JSON.parse(framesent.payload as string) as {
type: string;
};
const typeCases = ['publish', 'subscribe', 'unsubscribe', 'ping'];
expect(typeCases.includes(payload.type)).toBeTruthy();
expect(framesent.payload).not.toBeNull();
});
test('markdown button converts from markdown to the editor syntax json', async ({
@@ -104,10 +101,6 @@ test.describe('Doc Editor', () => {
nthRow: 2,
});
await expect(
page.getByText(`Your document "${doc}" has been saved.`),
).toBeVisible();
await expect(page.locator('h2').getByText(secondDoc)).toBeVisible();
await goToGridDoc(page, {
@@ -159,4 +152,31 @@ test.describe('Doc Editor', () => {
page.getByText('Read only, you cannot edit this document.'),
).toBeVisible();
});
test('it adds an image to the doc editor', async ({ page }) => {
await goToGridDoc(page);
const fileChooserPromise = page.waitForEvent('filechooser');
await page.locator('.bn-block-outer').last().fill('Hello World');
await page.keyboard.press('Enter');
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Resizable image with caption').click();
await page.getByText('Upload image').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(
path.join(__dirname, 'assets/logo-suite-numerique.png'),
);
const image = page.getByRole('img', { name: 'logo-suite-numerique.png' });
await expect(image).toBeVisible();
// Check src of image
expect(await image.getAttribute('src')).toMatch(
/http:\/\/localhost:8083\/media\/.*\/attachments\/.*.png/,
);
});
});
@@ -175,6 +175,11 @@ test.describe('Doc Export', () => {
name: 'Image',
})
.click();
await page
.getByRole('tab', {
name: 'Embed',
})
.click();
await page
.getByPlaceholder('Enter URL')
.fill('https://example.com/image.jpg');
@@ -49,111 +49,130 @@ test.describe('Documents Grid', () => {
nameColumn: 'Document name',
ordering: 'title',
cellNumber: 1,
orderDefault: '',
orderDesc: '&ordering=-title',
orderAsc: '&ordering=title',
},
{
nameColumn: 'Created at',
ordering: 'created_at',
cellNumber: 2,
orderDefault: '',
orderDesc: '&ordering=-created_at',
orderAsc: '&ordering=created_at',
},
{
nameColumn: 'Updated at',
ordering: 'updated_at',
cellNumber: 3,
orderDefault: '&ordering=-updated_at',
orderDesc: '&ordering=updated_at',
orderAsc: '',
},
].forEach(({ nameColumn, ordering, cellNumber }) => {
test(`checks datagrid ordering ${ordering}`, async ({ page }) => {
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes(`/documents/?page=1`) &&
response.status() === 200,
);
].forEach(
({
nameColumn,
ordering,
cellNumber,
orderDefault,
orderDesc,
orderAsc,
}) => {
test(`checks datagrid ordering ${ordering}`, async ({ page }) => {
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes(`/documents/?page=1${orderDefault}`) &&
response.status() === 200,
);
const responsePromiseOrderingDesc = page.waitForResponse(
(response) =>
response.url().includes(`/documents/?page=1&ordering=-${ordering}`) &&
response.status() === 200,
);
const responsePromiseOrderingDesc = page.waitForResponse(
(response) =>
response.url().includes(`/documents/?page=1${orderDesc}`) &&
response.status() === 200,
);
const responsePromiseOrderingAsc = page.waitForResponse(
(response) =>
response.url().includes(`/documents/?page=1&ordering=${ordering}`) &&
response.status() === 200,
);
const responsePromiseOrderingAsc = page.waitForResponse(
(response) =>
response.url().includes(`/documents/?page=1${orderAsc}`) &&
response.status() === 200,
);
const datagrid = page
.getByLabel('Datagrid of the documents page 1')
.getByRole('table');
const thead = datagrid.locator('thead');
// Checks the initial state
const datagrid = page
.getByLabel('Datagrid of the documents page 1')
.getByRole('table');
const thead = datagrid.locator('thead');
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
const docNameRow1 = datagrid
.getByRole('row')
.nth(1)
.getByRole('cell')
.nth(cellNumber);
const docNameRow2 = datagrid
.getByRole('row')
.nth(2)
.getByRole('cell')
.nth(cellNumber);
const docNameRow1 = datagrid
.getByRole('row')
.nth(1)
.getByRole('cell')
.nth(cellNumber);
const docNameRow2 = datagrid
.getByRole('row')
.nth(2)
.getByRole('cell')
.nth(cellNumber);
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
// Initial state
await expect(docNameRow1).toHaveText(/.*/);
await expect(docNameRow2).toHaveText(/.*/);
const initialDocNameRow1 = await docNameRow1.textContent();
const initialDocNameRow2 = await docNameRow2.textContent();
// Initial state
await expect(docNameRow1).toHaveText(/.*/);
await expect(docNameRow2).toHaveText(/.*/);
const initialDocNameRow1 = await docNameRow1.textContent();
const initialDocNameRow2 = await docNameRow2.textContent();
expect(initialDocNameRow1).toBeDefined();
expect(initialDocNameRow2).toBeDefined();
expect(initialDocNameRow1).toBeDefined();
expect(initialDocNameRow2).toBeDefined();
// Ordering ASC
await thead.getByText(nameColumn).click();
// Ordering ASC
await thead.getByText(nameColumn).click();
const responseOrderingAsc = await responsePromiseOrderingAsc;
expect(responseOrderingAsc.ok()).toBeTruthy();
const responseOrderingAsc = await responsePromiseOrderingAsc;
expect(responseOrderingAsc.ok()).toBeTruthy();
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
await expect(docNameRow1).toHaveText(/.*/);
await expect(docNameRow2).toHaveText(/.*/);
const textDocNameRow1Asc = await docNameRow1.textContent();
const textDocNameRow2Asc = await docNameRow2.textContent();
expect(
textDocNameRow1Asc &&
textDocNameRow2Asc &&
textDocNameRow1Asc.localeCompare(textDocNameRow2Asc, 'en', {
caseFirst: 'false',
ignorePunctuation: true,
}) <= 0,
).toBeTruthy();
await expect(docNameRow1).toHaveText(/.*/);
await expect(docNameRow2).toHaveText(/.*/);
const textDocNameRow1Asc = await docNameRow1.textContent();
const textDocNameRow2Asc = await docNameRow2.textContent();
expect(
textDocNameRow1Asc &&
textDocNameRow2Asc &&
textDocNameRow1Asc.localeCompare(textDocNameRow2Asc, 'en', {
caseFirst: 'false',
ignorePunctuation: true,
}) <= 0,
).toBeTruthy();
// Ordering Desc
await thead.getByText(nameColumn).click();
// Ordering Desc
await thead.getByText(nameColumn).click();
const responseOrderingDesc = await responsePromiseOrderingDesc;
expect(responseOrderingDesc.ok()).toBeTruthy();
const responseOrderingDesc = await responsePromiseOrderingDesc;
expect(responseOrderingDesc.ok()).toBeTruthy();
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
await expect(docNameRow1).toHaveText(/.*/);
await expect(docNameRow2).toHaveText(/.*/);
const textDocNameRow1Desc = await docNameRow1.textContent();
const textDocNameRow2Desc = await docNameRow2.textContent();
await expect(docNameRow1).toHaveText(/.*/);
await expect(docNameRow2).toHaveText(/.*/);
const textDocNameRow1Desc = await docNameRow1.textContent();
const textDocNameRow2Desc = await docNameRow2.textContent();
expect(
textDocNameRow1Desc &&
textDocNameRow2Desc &&
textDocNameRow1Desc.localeCompare(textDocNameRow2Desc, 'en', {
caseFirst: 'false',
ignorePunctuation: true,
}) >= 0,
).toBeTruthy();
});
});
expect(
textDocNameRow1Desc &&
textDocNameRow2Desc &&
textDocNameRow1Desc.localeCompare(textDocNameRow2Desc, 'en', {
caseFirst: 'false',
ignorePunctuation: true,
}) >= 0,
).toBeTruthy();
});
},
);
test('checks the pagination', async ({ page }) => {
const responsePromisePage1 = page.waitForResponse(
@@ -65,13 +65,7 @@ test.describe('Doc Header', () => {
});
test('it updates the doc', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(
page,
'doc-update',
browserName,
1,
true,
);
const [randomDoc] = await createDoc(page, 'doc-update', browserName, 1);
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
await page.getByLabel('Open the document options').click();
@@ -85,12 +79,7 @@ test.describe('Doc Header', () => {
page.locator('h2').getByText(`Update document "${randomDoc}"`),
).toBeVisible();
await expect(
page.getByRole('checkbox', { name: 'Is it public ?' }),
).toBeChecked();
await page.getByText('Document name').fill(`${randomDoc}-updated`);
await page.getByText('Is it public ?').click();
await page
.getByRole('button', {
@@ -116,8 +105,8 @@ test.describe('Doc Header', () => {
.click();
await expect(
page.getByRole('checkbox', { name: 'Is it public ?' }),
).not.toBeChecked();
page.getByRole('textbox', { name: 'Document name' }),
).toHaveValue(`${randomDoc}-updated`);
});
test('it deletes the doc', async ({ page, browserName }) => {
@@ -47,4 +47,13 @@ test.describe('Doc Routing: Not loggued', () => {
await keyCloakSignIn(page, browserName);
await expect(page).toHaveURL(/\/docs\/mocked-document-id\/$/);
});
test('The homepage redirects to login.', async ({ page }) => {
await page.goto('/');
await expect(
page.getByRole('button', {
name: 'Sign In',
}),
).toBeVisible();
});
});
@@ -0,0 +1,64 @@
import { expect, test } from '@playwright/test';
import { createDoc } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.describe('Doc Summary', () => {
test('it checks the doc summary', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-summary', browserName, 1);
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', {
name: 'Summary',
})
.click();
const panel = page.getByLabel('Document panel');
const editor = page.locator('.ProseMirror');
await editor.locator('.bn-block-outer').last().fill('/');
await page.getByText('Heading 1').click();
await page.keyboard.type('Hello World');
await page.locator('.bn-block-outer').last().click();
// Create space to fill the viewport
for (let i = 0; i < 6; i++) {
await page.keyboard.press('Enter');
}
await editor.locator('.bn-block-outer').last().fill('/');
await page.getByText('Heading 2').click();
await page.keyboard.type('Super World');
await page.locator('.bn-block-outer').last().click();
// Create space to fill the viewport
for (let i = 0; i < 4; i++) {
await page.keyboard.press('Enter');
}
await editor.locator('.bn-block-outer').last().fill('/');
await page.getByText('Heading 3').click();
await page.keyboard.type('Another World');
await expect(panel.getByText('Hello World')).toBeVisible();
await expect(panel.getByText('Super World')).toBeVisible();
await panel.getByText('Another World').click();
await expect(editor.getByText('Hello World')).not.toBeInViewport();
await panel.getByText('Back to top').click();
await expect(editor.getByText('Hello World')).toBeInViewport();
await panel.getByText('Go to bottom').click();
await expect(editor.getByText('Hello World')).not.toBeInViewport();
});
});
@@ -0,0 +1,96 @@
import { expect, test } from '@playwright/test';
import { createDoc, keyCloakSignIn } from './common';
test.describe('Doc Visibility', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('Make a public doc', async ({ page, browserName }) => {
const [docTitle] = await createDoc(
page,
'My new doc',
browserName,
1,
true,
);
const header = page.locator('header').first();
await header.locator('h2').getByText('Docs').click();
const datagrid = page
.getByLabel('Datagrid of the documents page 1')
.getByRole('table');
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
await expect(datagrid.getByText(docTitle)).toBeVisible();
const row = datagrid.getByRole('row').filter({
hasText: docTitle,
});
await expect(row.getByRole('cell').nth(0)).toHaveText('Public');
});
test('It checks the copy link button', async ({ page, browserName }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(
browserName === 'webkit',
'navigator.clipboard is not working with webkit and playwright',
);
await createDoc(page, 'My button copy doc', browserName, 1);
await page.getByRole('button', { name: 'Share' }).click();
await page.getByRole('button', { name: 'Copy link' }).click();
await expect(page.getByText('Link Copied !')).toBeVisible();
const handle = await page.evaluateHandle(() =>
navigator.clipboard.readText(),
);
const clipboardContent = await handle.jsonValue();
expect(clipboardContent).toMatch(page.url());
});
});
test.describe('Doc Visibility: Not loggued', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('A public doc is accessible even when not authentified.', async ({
page,
browserName,
}) => {
await page.goto('/');
await keyCloakSignIn(page, browserName);
const [docTitle] = await createDoc(
page,
'My new doc',
browserName,
1,
true,
);
await expect(
page.getByText('The document visiblitity has been updated.'),
).toBeVisible();
const urlDoc = page.url();
await page
.getByRole('button', {
name: 'Logout',
})
.click();
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
await page.goto(urlDoc);
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
});
});
@@ -22,6 +22,12 @@ test.describe('Header', () => {
/Marianne/i,
);
await expect(
header.getByRole('button', {
name: 'Logout',
}),
).toBeVisible();
await expect(header.getByAltText('Language Icon')).toBeVisible();
await expect(
@@ -68,12 +74,6 @@ test.describe('Header: Log out', () => {
await page.goto('/');
await keyCloakSignIn(page, browserName);
await page
.getByRole('button', {
name: 'My account',
})
.click();
await page
.getByRole('button', {
name: 'Logout',
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "1.2.0",
"version": "1.3.0",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
@@ -12,7 +12,7 @@
"test:ui::chromium": "yarn test:ui --project=chromium"
},
"devDependencies": {
"@playwright/test": "1.46.1",
"@playwright/test": "1.47.0",
"@types/node": "*",
"@types/pdf-parse": "1.1.4",
"eslint-config-impress": "*",
@@ -20,7 +20,7 @@
},
"dependencies": {
"convert-stream": "1.0.2",
"jsdom": "24.1.1",
"pdf-parse": "^1.1.1"
"jsdom": "25.0.0",
"pdf-parse": "1.1.1"
}
}
+11 -4
View File
@@ -32,12 +32,10 @@ export default defineConfig({
},
webServer: {
command: `cd ../.. && yarn app:${
process.env.CI ? 'start -p ' : 'dev --port '
} ${PORT}`,
command: !process.env.CI ? `cd ../.. && yarn app:dev --port ${PORT}` : '',
url: baseURL,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
reuseExistingServer: true,
},
/* Configure projects for major browsers */
@@ -50,6 +48,9 @@ export default defineConfig({
locale: 'en-US',
timezoneId: 'Europe/Paris',
storageState: 'playwright/.auth/user-chromium.json',
contextOptions: {
permissions: ['clipboard-read', 'clipboard-write'],
},
},
dependencies: ['setup'],
},
@@ -70,6 +71,12 @@ export default defineConfig({
locale: 'en-US',
timezoneId: 'Europe/Paris',
storageState: 'playwright/.auth/user-firefox.json',
launchOptions: {
firefoxUserPrefs: {
'dom.events.asyncClipboard.readText': true,
'dom.events.testing.asyncClipboard': true,
},
},
},
dependencies: ['setup'],
},
+3 -2
View File
@@ -1,3 +1,4 @@
NEXT_PUBLIC_THEME=dsfr
NEXT_PUBLIC_SIGNALING_URL=
NEXT_PUBLIC_API_ORIGIN=
NEXT_PUBLIC_Y_PROVIDER_URL=
NEXT_PUBLIC_MEDIA_URL=
NEXT_PUBLIC_THEME=openDesk
+2 -1
View File
@@ -1,3 +1,4 @@
NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
NEXT_PUBLIC_SIGNALING_URL=ws://localhost:4444
NEXT_PUBLIC_Y_PROVIDER_URL=ws://localhost:4444
NEXT_PUBLIC_MEDIA_URL=http://localhost:8083
NEXT_PUBLIC_SW_DEACTIVATED=true
@@ -1,5 +1,6 @@
server {
listen 8080;
listen 3000;
server_name localhost;
root /usr/share/nginx/html;
+396 -285
View File
@@ -1,14 +1,278 @@
const dsfrColors = {
'primary-100': '#f5f5fe',
'primary-150': '#F4F4FD',
'primary-200': '#ececfe',
'primary-300': '#e3e3fd',
'primary-400': '#cacafb',
'primary-500': '#6a6af4',
'primary-600': '#000091',
'primary-700': '#272747',
'primary-800': '#21213f',
'primary-900': '#1c1a36',
'secondary-100': '#fee9ea',
'secondary-200': '#fedfdf',
'secondary-300': '#fdbfbf',
'secondary-400': '#e1020f',
'secondary-500': '#c91a1f',
'secondary-600': '#5e2b2b',
'secondary-700': '#3b2424',
'secondary-800': '#341f1f',
'secondary-900': '#2b1919',
'greyscale-000': '#ffffff',
'greyscale-100': '#eeeeee',
'greyscale-200': '#e5e5e5',
'greyscale-300': '#e1e1e1',
'greyscale-400': '#dddddd',
'greyscale-500': '#cecece',
'greyscale-600': '#7b7b7b',
'greyscale-700': '#666666',
'greyscale-800': '#2a2a2a',
'greyscale-900': '#1e1e1e',
'success-text': '#1f8d49',
'success-100': '#dffee6',
'success-200': '#b8fec9',
'success-300': '#88fdaa',
'success-400': '#3bea7e',
'success-500': '#1f8d49',
'success-600': '#18753c',
'success-700': '#204129',
'success-800': '#1e2e22',
'success-900': '#19281d',
'info-text': '#0078f3',
'info-100': '#f4f6ff',
'info-200': '#e8edff',
'info-300': '#dde5ff',
'info-400': '#bdcdff',
'info-500': '#0078f3',
'info-600': '#0063cb',
'info-700': '#f4f6ff',
'info-800': '#222a3f',
'info-900': '#1d2437',
'warning-text': '#d64d00',
'warning-100': '#fff4f3',
'warning-200': '#ffe9e6',
'warning-300': '#ffded9',
'warning-400': '#ffbeb4',
'warning-500': '#d64d00',
'warning-600': '#b34000',
'warning-700': '#5e2c21',
'warning-800': '#3e241e',
'warning-900': '#361e19',
'danger-text': '#e1000f',
'danger-100': '#fef4f4',
'danger-200': '#fee9e9',
'danger-300': '#fddede',
'danger-400': '#fcbfbf',
'danger-500': '#e1000f',
'danger-600': '#c9191e',
'danger-700': '#642727',
'danger-800': '#412121',
'danger-900': '#3a1c1c',
};
const dsfr = {
theme: {
colors: dsfrColors,
font: {
families: {
accent: 'Marianne',
base: 'Marianne',
},
},
logo: {
src: '/assets/logo-gouv.svg',
widthHeader: '110px',
widthFooter: '220px',
alt: 'Gouvernement Logo',
},
},
components: {
alert: {
'border-radius': '0',
'background-color': 'var(--c--theme--colors--greyscale-000)',
},
button: {
'medium-height': '48px',
'border-radius': '0',
primary: {
background: {
color: 'var(--c--theme--colors--primary-600)',
'color-hover': '#1212ff',
'color-active': '#2323ff',
},
color: '#ffffff',
'color-hover': '#ffffff',
'color-active': '#ffffff',
},
'primary-text': {
background: {
'color-hover': 'var(--c--theme--colors--primary-100)',
'color-active': 'var(--c--theme--colors--primary-100)',
},
'color-hover': 'var(--c--theme--colors--primary-text)',
},
secondary: {
background: {
'color-hover': '#F6F6F6',
'color-active': '#EDEDED',
},
border: {
color: 'var(--c--theme--colors--primary-600)',
'color-hover': 'var(--c--theme--colors--primary-600)',
},
color: 'var(--c--theme--colors--primary-600)',
},
'tertiary-text': {
background: {
'color-hover': 'var(--c--theme--colors--primary-100)',
},
'color-hover': 'var(--c--theme--colors--primary-text)',
},
},
card: {
'box-shadow': '2px 2px 5px var(--c--theme--colors--greyscale-300)',
'title-color': 'var(--c--theme--colors--primary-600)',
},
datagrid: {
header: {
color: 'var(--c--theme--colors--primary-600)',
size: 'var(--c--theme--font--sizes--s)',
},
body: {
'background-color': 'transparent',
'background-color-hover': '#F4F4FD',
},
pagination: {
'background-color': 'transparent',
'background-color-active': 'var(--c--theme--colors--primary-300)',
},
},
'forms-datepicker': {
'border-radius': '0',
},
'forms-fileuploader': {
'border-radius': '0',
},
'forms-input': {
'background-color': 'var(--c--theme--colors--greyscale-100)',
'border-radius': '0',
'border-color': 'var(--c--theme--colors--greyscale-900)',
'border-width': '0 0 2px 0',
'border-color--focus': '#0974F6',
'border-color--hover': 'var(--c--theme--colors--greyscale-900)',
'label-color--focus':
'var(--c--components--forms-labelledbox--label-color--small)',
},
'forms-textarea': {
'background-color': 'var(--c--theme--colors--greyscale-100)',
'border-radius': '0',
'border-color': 'var(--c--theme--colors--greyscale-900)',
'border-width': '0 0 2px 0',
'border-color--focus': '#0974F6',
'border-color--hover': 'var(--c--theme--colors--greyscale-900)',
'label-color--focus':
'var(--c--components--forms-labelledbox--label-color--small)',
},
'forms-select': {
'background-color': 'var(--c--theme--colors--greyscale-100)',
'border-radius': '0',
'border-color': 'var(--c--theme--colors--greyscale-900)',
'border-width': '0 0 2px 0',
'border-color--focus': '#0974F6',
'border-color--hover': 'var(--c--theme--colors--greyscale-900)',
'label-color--focus':
'var(--c--components--forms-labelledbox--label-color--big)',
},
'forms-switch': {
'accent-color': '#2323ff',
},
'forms-checkbox': {
'accent-color': '#2323ff',
},
},
};
const dsfrColorsDark = {
'primary-100': '#1a1a2e',
'primary-150': '#21213f',
'primary-200': '#272747',
'primary-300': '#3b3b61',
'primary-400': '#535380',
'primary-500': '#6a6af4', // même teinte pour conserver une touche de couleur vive
'primary-600': '#8b8bf9', // légèrement plus claire pour le mode sombre
'primary-700': '#a1a1f5',
'primary-800': '#c1c1ff',
'primary-900': '#ececfe',
'secondary-100': '#2b1919',
'secondary-200': '#341f1f',
'secondary-300': '#3b2424',
'secondary-400': '#5e2b2b',
'secondary-500': '#c91a1f', // teinte conservée pour son intensité
'secondary-600': '#e6454a', // teinte plus vive en mode sombre
'secondary-700': '#f06062',
'secondary-800': '#fca7a9',
'secondary-900': '#fee9ea',
'greyscale-000': '#161616',
'greyscale-100': '#1e1e1e',
'greyscale-200': '#242424',
'greyscale-300': '#2a2a2a',
'greyscale-400': '#2f2f2f',
'greyscale-500': '#353535',
'greyscale-600': '#3a3a3a',
'greyscale-700': '#929292',
'greyscale-800': '#7b7b7b',
'greyscale-900': '#eeeeee',
'success-text': '#88fdaa', // rendre plus lumineux en mode sombre
'success-100': '#1e2e22',
'success-200': '#204129',
'success-300': '#18753c',
'success-400': '#1f8d49', // un peu plus vive
'success-500': '#3bea7e',
'success-600': '#66f2a1',
'success-700': '#88fdaa',
'success-800': '#b8fec9',
'success-900': '#dffee6',
'info-text': '#0078f3',
'info-100': '#1d2437',
'info-200': '#222a3f',
'info-300': '#293145',
'info-400': '#3b4c6b',
'info-500': '#0078f3', // teinte inchangée
'info-600': '#3391ff', // teinte plus claire pour le mode sombre
'info-700': '#66aaff',
'info-800': '#99ccff',
'info-900': '#cce5ff',
'warning-text': '#ffbeb4', // couleur plus douce pour être lisible
'warning-100': '#361e19',
'warning-200': '#3e241e',
'warning-300': '#5e2c21',
'warning-400': '#b34000', // toujours intense
'warning-500': '#d64d00',
'warning-600': '#ff6a34',
'warning-700': '#ff8b66',
'warning-800': '#ffb299',
'warning-900': '#ffe9e6',
'danger-text': '#fddede',
'danger-100': '#3a1c1c',
'danger-200': '#412121',
'danger-300': '#642727',
'danger-400': '#c9191e', // un peu plus lumineux
'danger-500': '#e1000f',
'danger-600': '#f5434a',
'danger-700': '#f76f71',
'danger-800': '#fdbfbf',
'danger-900': '#fee9e9',
};
const config = {
themes: {
default: {
theme: {
colors: {
'card-border': '#ededed',
'primary-bg': '#FAFAFA',
'primary-100': '#EDF5FA',
'primary-150': '#E5EEFA',
'info-150': '#E5EEFA',
},
font: {
sizes: {
ml: '0.938rem',
@@ -22,260 +286,134 @@ const config = {
h5: '1rem',
h6: '0.87rem',
},
weights: {
thin: 100,
extrabold: 800,
black: 900,
},
},
spacings: {
'0': '0',
none: '0',
auto: 'auto',
bx: '2.2rem',
full: '100%',
},
breakpoints: {
xxs: '320px',
xs: '480px',
},
logo: {
src: '',
widthHeader: '',
widthFooter: '',
alt: '',
},
},
components: {
datagrid: {
header: {
weight: 'var(--c--theme--font--weights--extrabold)',
size: 'var(--c--theme--font--sizes--ml)',
},
cell: {
color: 'var(--c--theme--colors--primary-500)',
size: 'var(--c--theme--font--sizes--ml)',
},
card: {
'box-shadow': 'none',
'title-color': 'var(--c--theme--colors--primary-600)',
},
'forms-checkbox': {
'background-color': {
hover: '#055fd214',
},
color: 'var(--c--theme--colors--primary-500)',
'font-size': 'var(--c--theme--font--sizes--ml)',
pill: {
background: 'var(--c--theme--colors--primary-600)',
color: 'var(--c--theme--colors--greyscale-000)',
weight: 'bold',
radius: '3px',
'padding-x': '4px',
'padding-y': '0',
},
'forms-datepicker': {
'border-color': 'var(--c--theme--colors--primary-500)',
'value-color': 'var(--c--theme--colors--primary-500)',
'border-radius': {
hover: 'var(--c--components--forms-datepicker--border-radius)',
focus: 'var(--c--components--forms-datepicker--border-radius)',
},
strip: {
color: 'var(--c--theme--colors--danger-500)',
},
'forms-field': {
color: 'var(--c--theme--colors--primary-500)',
'value-color': 'var(--c--theme--colors--primary-500)',
width: 'auto',
grid: {
color: 'var(--c--theme--colors--danger-900)',
},
'forms-input': {
'value-color': 'var(--c--theme--colors--primary-500)',
'border-color': 'var(--c--theme--colors--primary-500)',
color: {
error: 'var(--c--theme--colors--danger-500)',
'error-hover': 'var(--c--theme--colors--danger-500)',
'box-shadow-error-hover': 'var(--c--theme--colors--danger-500)',
},
header: {
background: 'var(--c--theme--colors--greyscale-000)',
'title-color': 'var(--c--theme--colors--primary-800)',
},
'forms-labelledbox': {
'label-color': {
small: 'var(--c--theme--colors--primary-500)',
'small-disabled': 'var(--c--theme--colors--greyscale-400)',
big: {
disabled: 'var(--c--theme--colors--greyscale-400)',
},
},
footer: {
background: 'var(--c--theme--colors--greyscale-000)',
},
'forms-select': {
'border-color': 'var(--c--theme--colors--primary-500)',
'border-color-disabled-hover':
'var(--c--theme--colors--greyscale-200)',
'border-radius': {
hover: 'var(--c--components--forms-select--border-radius)',
focus: 'var(--c--components--forms-select--border-radius)',
},
'font-size': 'var(--c--theme--font--sizes--ml)',
'menu-background-color': '#ffffff',
'item-background-color': {
hover: 'var(--c--theme--colors--primary-300)',
},
main: {
background: 'var(--c--theme--colors--greyscale-100)',
},
'forms-switch': {
'accent-color': 'var(--c--theme--colors--primary-400)',
},
'forms-textarea': {
'border-color': 'var(--c--components--forms-textarea--border-color)',
'border-color-hover':
'var(--c--components--forms-textarea--border-color)',
'border-radius': {
hover: 'var(--c--components--forms-textarea--border-radius)',
focus: 'var(--c--components--forms-textarea--border-radius)',
},
color: 'var(--c--theme--colors--primary-500)',
disabled: {
'border-color-hover': 'var(--c--theme--colors--greyscale-200)',
},
},
modal: {
'background-color': '#ffffff',
},
button: {
'border-radius': {
active: 'var(--c--components--button--border-radius)',
},
'medium-height': 'auto',
'small-height': 'auto',
success: {
color: 'white',
'color-disabled': 'white',
'color-hover': 'white',
background: {
color: 'var(--c--theme--colors--success-600)',
'color-disabled': 'var(--c--theme--colors--greyscale-300)',
'color-hover': 'var(--c--theme--colors--success-800)',
},
},
danger: {
'color-hover': 'white',
background: {
color: 'var(--c--theme--colors--danger-400)',
'color-hover': 'var(--c--theme--colors--danger-500)',
'color-disabled': 'var(--c--theme--colors--danger-100)',
},
},
primary: {
color: 'var(--c--theme--colors--primary-text)',
'color-active': 'var(--c--theme--colors--primary-text)',
background: {
color: 'var(--c--theme--colors--primary-400)',
'color-active': 'var(--c--theme--colors--primary-500)',
},
border: {
'color-active': 'transparent',
},
},
secondary: {
color: 'var(--c--theme--colors--primary-500)',
'color-hover': 'var(--c--theme--colors--primary-text)',
background: {
color: 'white',
'color-hover': 'var(--c--theme--colors--primary-700)',
},
border: {
color: 'var(--c--theme--colors--primary-200)',
},
},
tertiary: {
color: 'var(--c--theme--colors--primary-text)',
'color-disabled': 'var(--c--theme--colors--greyscale-600)',
background: {
'color-hover': 'var(--c--theme--colors--primary-100)',
'color-disabled': 'var(--c--theme--colors--greyscale-200)',
},
},
disabled: {
color: 'white',
background: {
color: '#b3cef0',
},
},
languagePicker: {
image: '/assets/icon-language.svg',
},
},
},
dsfr: {
dsfr: dsfr,
dsfrDark: {
...dsfr,
theme: {
...dsfr.theme,
colors: {
...dsfr.theme.colors,
...dsfrColorsDark,
},
logo: {
...dsfr.theme.logo,
src: '/assets/logo-gouv-dark.svg',
},
},
components: {
...dsfr.components,
button: {
...dsfr.components.button,
primary: {
background: {
color: 'var(--c--theme--colors--primary-600)',
'color-hover': '#1212ff',
'color-active': '#2323ff',
},
color: 'var(--c--theme--colors--greyscale-000)',
'color-hover': '#ffffff',
'color-active': '#ffffff',
},
},
card: {
...dsfr.components.card,
'title-color': 'var(--c--theme--colors--primary-900)',
},
header: {
background: 'var(--c--theme--colors--greyscale-100)',
'title-color': 'var(--c--theme--colors--greyscale-900)',
},
footer: {
background: 'var(--c--theme--colors--greyscale-100)',
},
main: {
background: 'var(--c--theme--colors--greyscale-000)',
},
languagePicker: {
image: '/assets/icon-language-dark.svg',
},
},
},
openDesk: {
theme: {
colors: {
'card-border': '#ededed',
'primary-text': '#000091',
'primary-100': '#f5f5fe',
'primary-150': '#F4F4FD',
'primary-200': '#ececfe',
'primary-300': '#e3e3fd',
'primary-400': '#cacafb',
'primary-500': '#6a6af4',
'primary-600': '#000091',
'primary-700': '#272747',
'primary-800': '#21213f',
'primary-900': '#1c1a36',
'secondary-text': '#FFFFFF',
'secondary-100': '#fee9ea',
'secondary-200': '#fedfdf',
'secondary-300': '#fdbfbf',
'secondary-400': '#e1020f',
'secondary-500': '#c91a1f',
'secondary-600': '#5e2b2b',
'secondary-700': '#3b2424',
'secondary-800': '#341f1f',
'secondary-900': '#2b1919',
'greyscale-text': '#303C4B',
'greyscale-000': '#f6f6f6',
'greyscale-100': '#eeeeee',
'greyscale-200': '#e5e5e5',
'greyscale-300': '#e1e1e1',
'greyscale-400': '#dddddd',
'greyscale-500': '#cecece',
'greyscale-600': '#7b7b7b',
'greyscale-700': '#666666',
'greyscale-800': '#2a2a2a',
'primary-100': '#F7F5FF',
'primary-200': '#ECE7FE',
'primary-300': '#DCD2FE',
'primary-400': '#C8B9FD',
'primary-500': '#8E75FA',
'primary-600': '#7051FA',
'primary-700': '#571EFA',
'primary-800': '#4519C2',
'primary-900': '#341291',
'secondary-100': '#EDFDFB',
'secondary-200': '#BFF9F2',
'secondary-300': '#71EFE1',
'secondary-400': '#00E6CC',
'secondary-500': '#00A896',
'secondary-600': '#008A7B',
'secondary-700': '#006C60',
'secondary-800': '#00564D',
'secondary-900': '#004039',
'greyscale-000': '#ffffff',
'greyscale-100': '#EEEFF2',
'greyscale-200': '#D3D7DE',
'greyscale-300': '#B6BCC8',
'greyscale-400': '#7C879C',
'greyscale-500': '#637089',
'greyscale-600': '#4D5B79',
'greyscale-700': '#364768',
'greyscale-800': '#203257',
'greyscale-900': '#1e1e1e',
'success-text': '#1f8d49',
'success-100': '#dffee6',
'success-200': '#b8fec9',
'success-300': '#88fdaa',
'success-400': '#3bea7e',
'success-500': '#1f8d49',
'success-600': '#18753c',
'success-700': '#204129',
'success-800': '#1e2e22',
'success-900': '#19281d',
'info-text': '#0078f3',
'info-100': '#f4f6ff',
'info-200': '#e8edff',
'info-300': '#dde5ff',
'info-400': '#bdcdff',
'info-500': '#0078f3',
'info-600': '#0063cb',
'info-700': '#f4f6ff',
'info-800': '#222a3f',
'info-900': '#1d2437',
'warning-text': '#d64d00',
'warning-100': '#fff4f3',
'warning-200': '#ffe9e6',
'warning-300': '#ffded9',
'warning-400': '#ffbeb4',
'warning-500': '#d64d00',
'warning-600': '#b34000',
'warning-700': '#5e2c21',
'warning-800': '#3e241e',
'warning-900': '#361e19',
'danger-text': '#e1000f',
'danger-100': '#fef4f4',
'danger-200': '#fee9e9',
'danger-300': '#fddede',
'danger-400': '#fcbfbf',
'danger-500': '#e1000f',
'danger-600': '#c9191e',
'danger-700': '#642727',
'danger-800': '#412121',
'danger-900': '#3a1c1c',
},
font: {
families: {
accent: 'Marianne',
base: 'Marianne',
accent: 'Open Sans',
base: 'Open Sans',
},
},
logo: {
src: '/assets/logo-gouv.svg',
src: '/assets/logo-opendesk.svg',
widthHeader: '110px',
widthFooter: '220px',
alt: 'Gouvernement Logo',
@@ -284,15 +422,18 @@ const config = {
components: {
alert: {
'border-radius': '0',
'background-color': 'var(--c--theme--colors--greyscale-000)',
},
button: {
'medium-height': '48px',
'border-radius': '4px',
'border-radius': '8px',
'border-radius--active': '8px',
'font-weight': '600',
primary: {
background: {
color: 'var(--c--theme--colors--primary-text)',
'color-hover': '#1212ff',
'color-active': '#2323ff',
color: 'var(--c--theme--colors--primary-700)',
'color-hover': 'var(--c--theme--colors--primary-900)',
'color-active': 'var(--c--theme--colors--primary-900)',
},
color: '#ffffff',
'color-hover': '#ffffff',
@@ -314,7 +455,7 @@ const config = {
color: 'var(--c--theme--colors--primary-600)',
'color-hover': 'var(--c--theme--colors--primary-600)',
},
color: 'var(--c--theme--colors--primary-text)',
color: 'var(--c--theme--colors--primary-600)',
},
'tertiary-text': {
background: {
@@ -323,62 +464,32 @@ const config = {
'color-hover': 'var(--c--theme--colors--primary-text)',
},
},
card: {
'title-color': 'var(--c--theme--colors--greyscale-900)',
},
datagrid: {
header: {
color: 'var(--c--theme--colors--primary-text)',
color: 'var(--c--theme--colors--greyscale-900)',
size: 'var(--c--theme--font--sizes--s)',
},
body: {
'background-color': 'transparent',
'background-color-hover': '#F4F4FD',
},
pagination: {
'background-color': 'transparent',
'background-color-active': 'var(--c--theme--colors--primary-300)',
},
},
'forms-checkbox': {
'border-radius': '0',
color: 'var(--c--theme--colors--primary-text)',
pill: {
background: 'var(--c--theme--colors--primary-300)',
color: 'var(--c--theme--colors--greyscale-900)',
weight: '500',
radius: '8px',
'padding-x': '16px',
'padding-y': '2px',
},
'forms-datepicker': {
'border-radius': '0',
strip: {
color: 'var(--c--theme--colors--primary-300)',
},
'forms-fileuploader': {
'border-radius': '0',
},
'forms-field': {
color: 'var(--c--theme--colors--primary-text)',
},
'forms-input': {
'border-radius': '4px',
'background-color': '#ffffff',
'border-color': 'var(--c--theme--colors--primary-text)',
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
'value-color': 'var(--c--theme--colors--primary-text)',
'font-size': '14px',
},
'forms-labelledbox': {
'label-color': {
big: 'var(--c--theme--colors--primary-text)',
},
},
'forms-select': {
'item-font-size': '14px',
'border-radius': '4px',
'border-radius-hover': '4px',
'background-color': '#ffffff',
'border-color': 'var(--c--theme--colors--primary-text)',
'border-color-hover': 'var(--c--theme--colors--primary-text)',
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
},
'forms-switch': {
'handle-border-radius': '2px',
'rail-border-radius': '4px',
'accent-color': 'var(--c--theme--colors--primary-text)',
},
'forms-textarea': {
'border-radius': '0',
grid: {
color: 'var(--c--theme--colors--greyscale-500)',
},
},
},
+1 -1
View File
@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
+16 -13
View File
@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "1.2.0",
"version": "1.3.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -19,35 +19,37 @@
"@blocknote/mantine": "*",
"@blocknote/react": "*",
"@gouvfr-lasuite/integration": "1.0.2",
"@openfun/cunningham-react": "2.9.3",
"@tanstack/react-query": "5.51.24",
"i18next": "23.14.0",
"@hocuspocus/provider": "2.13.5",
"@openfun/cunningham-react": "2.9.4",
"@tanstack/react-query": "5.55.4",
"i18next": "23.15.1",
"idb": "8.0.0",
"lodash": "4.17.21",
"luxon": "3.5.0",
"next": "14.2.5",
"next": "14.2.9",
"react": "*",
"react-aria-components": "1.3.1",
"react-aria-components": "1.3.3",
"react-dom": "*",
"react-i18next": "15.0.1",
"react-select": "5.8.0",
"styled-components": "6.1.12",
"y-webrtc": "10.3.0",
"sass": "^1.78.0",
"styled-components": "6.1.13",
"y-protocols": "1.0.6",
"yjs": "*",
"zustand": "4.5.5"
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.51.24",
"@tanstack/react-query-devtools": "5.55.4",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "6.4.8",
"@testing-library/react": "16.0.0",
"@testing-library/jest-dom": "6.5.0",
"@testing-library/react": "16.0.1",
"@testing-library/user-event": "14.5.2",
"@types/jest": "29.5.12",
"@types/lodash": "4.17.7",
"@types/luxon": "3.4.2",
"@types/node": "*",
"@types/react": "18.3.3",
"@types/react": "18.3.5",
"@types/react-dom": "*",
"cross-env": "*",
"dotenv": "16.4.5",
@@ -57,10 +59,11 @@
"jest-environment-jsdom": "29.7.0",
"node-fetch": "2.7.0",
"prettier": "3.3.3",
"stylelint": "16.8.2",
"stylelint": "16.9.0",
"stylelint-config-standard": "36.0.1",
"stylelint-prettier": "5.0.2",
"typescript": "*",
"webpack": "5.94.0",
"workbox-webpack-plugin": "7.1.0"
}
}
@@ -0,0 +1,14 @@
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.334 6.66683L15.2673 14.0002H13.8307L13.03 12.0002H10.3033L9.50398 14.0002H8.06798L11.0007 6.66683H12.334ZM6.66732 1.3335V2.66683H10.6673V4.00016H9.35532C8.84106 5.54821 8.0203 6.97684 6.94198 8.20083C7.42285 8.6299 7.94444 9.01104 8.49932 9.33883L7.99865 10.5908C7.28233 10.1846 6.61238 9.70149 6.00065 9.15016C4.80971 10.228 3.39934 11.035 1.86665 11.5155L1.50932 10.2295C2.82254 9.81077 4.03266 9.11972 5.06065 8.2015C4.29978 7.34012 3.66603 6.37434 3.17865 5.3335H4.67198C5.04355 6.0194 5.4891 6.66257 6.00065 7.2515C6.83406 6.2909 7.49085 5.19037 7.94065 4.00083L1.33398 4.00016V2.66683H5.33398V1.3335H6.66732ZM11.6673 8.59016L10.836 10.6668H12.4973L11.6673 8.59016Z"
fill="#a1a1f5"
/>
</svg>

After

Width:  |  Height:  |  Size: 877 B

Before

Width:  |  Height:  |  Size: 877 B

After

Width:  |  Height:  |  Size: 877 B

@@ -0,0 +1,244 @@
<?xml version="1.0" encoding="utf-8"?>
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="30 24 260 146.5"
>
<style type="text/css">
.st0 {
fill: #ffffff;
}
.st1 {
fill: #000091;
}
.st2 {
fill: #e1000f;
}
.st3 {
fill: #9c9b9b;
}
</style>
<g id="Calque_1">
<path
id="Devise_Républicaine_1_"
fill="#ffffff"
d="M100.5,150.8c0.6,0,1.1,0.4,0.8,1.5l-2.7,0.6C99.1,151.8,99.9,150.8,100.5,150.8 M102,155.2
h-0.5c-0.7,0.8-1.4,1.4-2.1,1.4c-0.7,0-1.1-0.4-1.1-1.4c0-0.4,0-0.8,0.1-1.2l4.3-1.4c0.8-2-0.2-2.9-1.4-2.9c-2,0-4.4,3.4-4.4,6.4
c0,1.3,0.6,2.1,1.6,2.1C99.8,158.2,101,157,102,155.2 M101.2,148.9l3-2.8v-0.3h-1.6l-1.9,3.2H101.2z M91.9,150.9h1.4l-2.3,6.2
c-0.2,0.5,0.1,1.1,0.6,1.1c1.3,0,3.4-1.3,4.1-3h-0.4c-0.6,0.6-1.7,1.4-2.6,1.6l2.1-5.8h2.1l0.3-0.9h-2.1l0.8-2.2h-0.8l-1.5,2.2
l-1.8,0.2V150.9z M89.9,150.6c0.2-0.6-0.2-0.9-0.5-0.9c-1.2,0-2.7,1.1-3.3,2.7h0.4c0.4-0.6,1.1-1.2,1.7-1.3l-2.4,6.2
c-0.2,0.6,0.2,0.9,0.5,0.9c1.2,0,2.6-1.1,3.2-2.7h-0.4c-0.4,0.6-1.1,1.2-1.7,1.3L89.9,150.6z M90.4,147.5c0.6,0,1-0.5,1-1
c0-0.6-0.5-1-1-1c-0.6,0-1,0.5-1,1C89.3,147,89.8,147.5,90.4,147.5 M76.6,157c-0.3,0.7,0,1.2,0.7,1.2c0.4,0,0.6-0.1,0.8-0.6
l1.7-4.4c0.8-0.9,2.3-1.9,3-1.9c0.5,0,0.4,0.4,0.1,0.9l-2.5,4.9c-0.2,0.5,0.1,1.1,0.6,1.1c1.2,0,2.7-1.1,3.3-2.7h-0.4
c-0.4,0.6-1.1,1.2-1.7,1.3l2.2-4.4c0.3-0.6,0.4-1.1,0.4-1.5c0-0.7-0.4-1.2-1.2-1.2c-1.1,0-2.2,1.2-3.5,2.7v-1.2
c0-0.8-0.3-1.6-1-1.6c-0.5,0-0.9,0.4-1.3,1v0.2c0.8,0,1.2,1.2,0.6,2.4L76.6,157z M76.6,151.6c0.3-1,0.1-1.9-0.6-1.9
c-0.9,0-1.2,0.7-2.1,2.7v-1.2c0-0.8-0.3-1.6-1-1.6c-0.9,0-1.7,1.4-2.3,2.7H71c0.4-0.6,0.8-1,1.1-1c0.4,0,0.6,0.6,0,1.9l-1.7,3.7
c-0.3,0.7,0,1.2,0.7,1.2c0.4,0,0.6-0.1,0.8-0.6l1.7-4.4c0.5-0.6,0.9-1.1,1.4-1.6H76.6z M67,150.8c0.6,0,1.1,0.4,0.8,1.5l-2.7,0.6
C65.6,151.8,66.4,150.8,67,150.8 M68.5,155.2H68c-0.7,0.8-1.4,1.4-2.1,1.4c-0.7,0-1.1-0.4-1.1-1.4c0-0.4,0-0.8,0.1-1.2l4.3-1.4
c0.8-2-0.2-2.9-1.4-2.9c-2,0-4.4,3.4-4.4,6.4c0,1.3,0.6,2.1,1.6,2.1C66.3,158.2,67.5,157,68.5,155.2 M58.3,150.9h1.4l-2.3,6.2
c-0.2,0.5,0.1,1.1,0.6,1.1c1.3,0,3.4-1.3,4.1-3h-0.4c-0.6,0.6-1.7,1.4-2.6,1.6l2.1-5.8h2.1l0.3-0.9h-2.1l0.8-2.2h-0.8l-1.5,2.2
l-1.8,0.2V150.9z M50.5,155.7c0-1.9,2.1-4.5,3.3-4.5c0.3,0,0.5,0,0.7,0.1l-1.2,3.3c-0.7,0.9-1.8,1.9-2.3,1.9
C50.7,156.6,50.5,156.3,50.5,155.7 M57,149.3l-0.7,0l-0.7,0.7h-0.2c-3.6,0-6.6,4-6.6,6.9c0,0.8,0.5,1.3,1.3,1.3
c0.9,0,1.8-1.3,2.8-2.7l0,0.5c-0.1,1.4,0.3,2.2,1.1,2.2c0.9,0,1.7-1.4,2.3-2.7h-0.4c-0.4,0.6-0.8,1-1.1,1c-0.3,0-0.6-0.6,0-1.9
L57,149.3z M49.5,151.6c0.3-1,0.1-1.9-0.6-1.9c-0.9,0-1.2,0.7-2.1,2.7v-1.2c0-0.8-0.3-1.6-1-1.6c-0.9,0-1.7,1.4-2.3,2.7h0.4
c0.4-0.6,0.8-1,1.1-1c0.4,0,0.6,0.6,0,1.9l-1.7,3.7c-0.3,0.7,0,1.2,0.7,1.2c0.4,0,0.6-0.1,0.8-0.6l1.7-4.4c0.5-0.6,0.9-1.1,1.4-1.6
H49.5z M37.5,157.9l0.2-0.5c-2.1-0.4-2.3-0.4-1.5-2.6l0.8-2.3h2.3c1,0,1,0.4,0.9,1.5h0.6l1.4-3.8h-0.6c-0.5,0.9-0.9,1.5-2,1.5h-2.3
l1.2-3.3c0.4-1,0.6-1.3,2-1.3h1c1.4,0,1.6,0.4,1.6,1.9h0.6l0.5-2.6h-8.6l-0.2,0.5c1.7,0.3,1.8,0.5,1,2.6l-1.9,5.1
c-0.8,2.1-1.1,2.3-3,2.6l-0.1,0.5H37.5z M79.7,131.4c0.6,0,1.1,0.4,0.8,1.5l-2.7,0.6C78.3,132.3,79.1,131.4,79.7,131.4 M81.2,135.7
h-0.5c-0.7,0.8-1.4,1.4-2.1,1.4c-0.7,0-1.1-0.4-1.1-1.4c0-0.4,0-0.8,0.1-1.2l4.3-1.4c0.8-2-0.2-2.9-1.4-2.9c-2,0-4.4,3.4-4.4,6.4
c0,1.3,0.6,2.1,1.6,2.1C79,138.7,80.2,137.6,81.2,135.7 M80.3,129.4l3-2.8v-0.3h-1.6l-1.9,3.2H80.3z M71,131.4h1.4l-2.3,6.2
c-0.2,0.5,0.1,1.1,0.6,1.1c1.3,0,3.4-1.3,4.1-3h-0.4c-0.6,0.6-1.7,1.4-2.6,1.6l2.1-5.8h2.1l0.3-0.9h-2.1l0.8-2.2h-0.8l-1.5,2.2
l-1.8,0.2V131.4z M69.1,131.1c0.2-0.6-0.2-0.9-0.5-0.9c-1.2,0-2.7,1.1-3.3,2.7h0.4c0.4-0.6,1.1-1.2,1.7-1.3l-2.4,6.2
c-0.2,0.6,0.2,0.9,0.5,0.9c1.2,0,2.6-1.1,3.2-2.7h-0.4c-0.4,0.6-1.1,1.2-1.7,1.3L69.1,131.1z M69.5,128c0.6,0,1-0.5,1-1
c0-0.6-0.5-1-1-1c-0.6,0-1,0.5-1,1C68.5,127.6,68.9,128,69.5,128 M61.2,137.3l4.3-11.4l-0.1-0.2l-2.7,0.3v0.3l0.5,0.4
c0.5,0.4,0.3,0.7-0.1,1.9l-3.4,8.9c-0.2,0.5,0.1,1.1,0.6,1.1c1.2,0,2.6-1.1,3.2-2.7h-0.4C62.7,136.6,61.8,137.2,61.2,137.3
M53,136.3c0-1.9,2.1-4.5,3.3-4.5c0.3,0,0.5,0,0.7,0.1l-1.2,3.3c-0.7,0.9-1.8,1.9-2.3,1.9C53.1,137.1,53,136.8,53,136.3
M59.5,129.9l-0.7,0l-0.7,0.7H58c-3.6,0-6.6,4-6.6,6.9c0,0.8,0.5,1.3,1.3,1.3c0.9,0,1.8-1.3,2.8-2.7l0,0.5
c-0.1,1.4,0.3,2.2,1.1,2.2c0.9,0,1.7-1.4,2.3-2.7h-0.4c-0.4,0.6-0.8,1-1.1,1c-0.3,0-0.6-0.6,0-1.9L59.5,129.9z M43.7,140.2
c0-0.8,0.8-1.3,1.9-1.8c0.4,0.2,0.9,0.4,1.7,0.6c1.2,0.4,1.6,0.5,1.6,0.9c0,0.8-1.3,1.3-3.1,1.3C44.4,141.3,43.7,141,43.7,140.2
M46.9,135.2c-0.5,0-0.7-0.4-0.7-0.9c0-1.4,0.7-3.4,1.9-3.4c0.5,0,0.7,0.4,0.7,0.9C48.8,133.1,48,135.2,46.9,135.2 M50.3,139.4
c0-1-0.9-1.4-2.3-1.8c-1.2-0.4-1.8-0.5-1.8-0.9c0-0.3,0.3-0.7,0.8-1c2-0.1,3.4-1.9,3.4-3.5c0-0.3-0.1-0.6-0.2-0.9h1.7l0.3-0.9h-2.7
c-0.3-0.2-0.7-0.3-1.1-0.3c-2.2,0-3.6,1.9-3.6,3.4c0,1.2,0.7,1.9,1.7,2.1c-1,0.5-1.6,1-1.6,1.6c0,0.4,0.1,0.6,0.4,0.8
c-2.3,0.7-3.3,1.5-3.3,2.6c0,1.1,1.4,1.5,3.1,1.5C47.9,142.3,50.3,140.7,50.3,139.4 M39.4,132.8c1,0,1,0.4,0.9,1.5h0.6l1.4-3.8
h-0.6c-0.5,0.9-0.9,1.5-2,1.5h-2.3l1.1-3.1c0.4-1,0.6-1.2,2-1.2h1c1.4,0,1.6,0.4,1.6,1.8h0.6l0.5-2.6h-8.6l-0.2,0.5
c1.7,0.3,1.8,0.5,1,2.6l-1.9,5.1c-0.8,2.1-1.1,2.3-3,2.6l-0.1,0.5H41l1.7-2.7H42c-1.1,1-2.5,2-4.4,2c-2.5,0-2.3-0.1-1.5-2.4
l0.9-2.5H39.4z M40.6,126.2l3-2.1v-0.3h-1.8l-1.7,2.4H40.6z M78.7,111.9c0.6,0,1.1,0.4,0.8,1.5l-2.7,0.6
C77.3,112.8,78,111.9,78.7,111.9 M80.2,116.2h-0.5c-0.7,0.8-1.4,1.4-2.1,1.4c-0.7,0-1.1-0.4-1.1-1.4c0-0.4,0-0.8,0.1-1.2l4.3-1.4
c0.8-2-0.2-2.9-1.4-2.9c-2,0-4.4,3.4-4.4,6.4c0,1.3,0.6,2.1,1.6,2.1C77.9,119.2,79.2,118.1,80.2,116.2 M79.3,110l3-2.8v-0.3h-1.6
l-1.9,3.2H79.3z M70.4,111.9h1.1l-2.3,6.2c-0.2,0.5,0.1,1.1,0.6,1.1c1.3,0,3.4-1.3,4.1-3h-0.4c-0.6,0.6-1.7,1.4-2.6,1.6l2.1-5.8
h2.1l0.3-0.9h-2.1l0.8-2.2h-0.8l-1.5,2.2l-1.5,0.2V111.9z M69.2,112.6c0.3-1,0.1-1.9-0.6-1.9c-0.9,0-1.2,0.7-2.1,2.7v-1.2
c0-0.8-0.3-1.6-1-1.6c-0.9,0-1.7,1.4-2.3,2.7h0.4c0.4-0.6,0.8-1,1.1-1c0.4,0,0.6,0.6,0,1.9l-1.7,3.7c-0.3,0.7,0,1.2,0.7,1.2
c0.4,0,0.6-0.1,0.8-0.6l1.7-4.4c0.5-0.6,0.9-1.1,1.4-1.6H69.2z M59.7,111.9c0.6,0,1.1,0.4,0.8,1.5l-2.7,0.6
C58.3,112.8,59.1,111.9,59.7,111.9 M61.2,116.2h-0.5c-0.7,0.8-1.4,1.4-2.1,1.4c-0.7,0-1.1-0.4-1.1-1.4c0-0.4,0-0.8,0.1-1.2l4.3-1.4
c0.8-2-0.2-2.9-1.4-2.9c-2,0-4.4,3.4-4.4,6.4c0,1.3,0.6,2.1,1.6,2.1C59,119.2,60.2,118.1,61.2,116.2 M50.6,118c-0.4,0-1-0.4-1-0.7
c0-0.1,0.2-0.6,0.4-1.2l0.7-1.9c0.7-0.9,1.9-1.8,2.5-1.8c0.4,0,0.7,0.2,0.7,0.8C53.9,114.9,52.3,118,50.6,118 M55.5,112.5
c0-1.3-0.5-1.7-1.3-1.7c-1.1,0-2.1,1.2-3.1,2.6l2.6-6.8l-0.1-0.2l-2.7,0.3v0.3l0.5,0.4c0.5,0.4,0.3,0.8-0.1,1.9l-2.8,7.2
c-0.2,0.5-0.5,1.2-0.5,1.3c0,0.7,1,1.4,1.9,1.4C52,119.2,55.5,115.4,55.5,112.5 M47,111.6c0.2-0.6-0.2-0.9-0.5-0.9
c-1.2,0-2.7,1.1-3.3,2.7h0.4c0.4-0.6,1.1-1.2,1.7-1.3l-2.4,6.2c-0.2,0.6,0.2,0.9,0.5,0.9c1.2,0,2.6-1.1,3.2-2.7h-0.4
c-0.4,0.6-1.1,1.2-1.7,1.3L47,111.6z M47.5,108.5c0.6,0,1-0.5,1-1c0-0.6-0.5-1-1-1c-0.6,0-1,0.5-1,1
C46.4,108.1,46.9,108.5,47.5,108.5 M41.2,107.5h-5.7l-0.2,0.5c1.7,0.3,1.8,0.5,1,2.6l-1.8,5.1c-0.8,2.1-1.1,2.3-3,2.6l-0.1,0.5H40
l1.9-3.3h-0.7c-1.1,1.2-2.3,2.6-4.2,2.6c-1.4,0-1.6-0.2-0.8-2.4l1.8-5.1c0.8-2.1,1.1-2.3,3-2.6L41.2,107.5z"
/>
<path
d="M40.9,88.2c1,0,1.9-0.2,2.7-0.5c0.8-0.3,1.5-0.8,2.1-1.3v-3.5h-5.3v-3.8h9.4v8.8c-1,1.3-2.2,2.3-3.8,3.1s-3.3,1.2-5.2,1.2
c-1.7,0-3.2-0.3-4.6-0.9c-1.4-0.6-2.6-1.4-3.6-2.4c-1-1-1.8-2.1-2.3-3.5c-0.6-1.3-0.8-2.7-0.8-4.2c0-1.5,0.3-2.9,0.8-4.2
c0.5-1.3,1.3-2.5,2.2-3.5c1-1,2.1-1.8,3.5-2.4s2.8-0.9,4.5-0.9c1.9,0,3.5,0.4,5,1.2c1.5,0.8,2.7,1.8,3.7,3L46,77
c-0.6-0.8-1.3-1.5-2.3-2.1s-2-0.8-3.1-0.8c-1,0-1.9,0.2-2.7,0.5c-0.8,0.4-1.5,0.9-2.1,1.5c-0.6,0.6-1,1.4-1.4,2.2
c-0.3,0.9-0.5,1.8-0.5,2.8s0.2,1.9,0.5,2.8c0.3,0.9,0.8,1.6,1.4,2.2c0.6,0.6,1.4,1.1,2.2,1.5C39,88,39.9,88.2,40.9,88.2z M64,70.3
c1.6,0,3.1,0.3,4.4,0.9s2.5,1.4,3.5,2.4c1,1,1.7,2.1,2.2,3.5c0.5,1.3,0.8,2.7,0.8,4.2c0,1.5-0.3,2.9-0.8,4.2
c-0.5,1.3-1.3,2.5-2.2,3.5c-1,1-2.1,1.8-3.5,2.4s-2.8,0.9-4.4,0.9c-1.6,0-3.1-0.3-4.5-0.9s-2.5-1.4-3.5-2.4c-1-1-1.7-2.1-2.2-3.5
c-0.5-1.3-0.8-2.7-0.8-4.2c0-1.5,0.3-2.9,0.8-4.2c0.5-1.3,1.3-2.5,2.2-3.5c1-1,2.1-1.8,3.5-2.4S62.4,70.3,64,70.3z M64,88.2
c1,0,1.9-0.2,2.7-0.5c0.8-0.4,1.5-0.9,2.1-1.5c0.6-0.6,1-1.4,1.4-2.2s0.5-1.8,0.5-2.8s-0.2-1.9-0.5-2.8c-0.3-0.9-0.8-1.6-1.4-2.2
c-0.6-0.6-1.3-1.1-2.1-1.5c-0.8-0.4-1.7-0.5-2.7-0.5c-1,0-1.9,0.2-2.7,0.5c-0.8,0.4-1.5,0.9-2.1,1.5c-0.6,0.6-1,1.4-1.4,2.2
c-0.3,0.9-0.5,1.8-0.5,2.8s0.2,1.9,0.5,2.8c0.3,0.9,0.8,1.6,1.4,2.2c0.6,0.6,1.3,1.1,2.1,1.5C62.1,88,63,88.2,64,88.2z M91.1,83.8
V70.9h4.2v12.6c0,2.7-0.8,4.8-2.3,6.4c-1.5,1.5-3.5,2.3-6.1,2.3c-2.6,0-4.6-0.8-6.1-2.3s-2.2-3.7-2.2-6.4V70.9h4.2v12.9
c0,1.4,0.4,2.5,1.1,3.2c0.7,0.8,1.8,1.2,3.1,1.2c1.3,0,2.3-0.4,3-1.2C90.8,86.3,91.1,85.2,91.1,83.8z M97.9,70.9h4.5l6.1,16.1
l6.1-16.1h4.5l-7.8,20.7h-5.5L97.9,70.9z M122.2,91.5V70.9h12v3.6h-7.8v4.8h6.7v3.6h-6.7V88h7.8v3.6H122.2z M139.1,91.5V70.9h6.3
c2.3,0,4.1,0.6,5.4,1.7c1.3,1.1,2,2.6,2,4.5c0,1.2-0.3,2.3-0.9,3.2c-0.6,0.9-1.4,1.6-2.4,2.1l6.5,9.1h-5l-5.5-8.3h-2.2v8.3H139.1z
M145.7,74.4h-2.4v5.2h2.4c0.9,0,1.6-0.2,2.1-0.7c0.5-0.5,0.7-1.1,0.7-1.9c0-0.8-0.2-1.4-0.7-1.9C147.2,74.7,146.6,74.4,145.7,74.4
z M158.6,91.5V70.9h5.4l9.2,14.8V70.9h4.2v20.7H172l-9.2-14.8v14.8H158.6z M183.1,91.5V70.9h12v3.6h-7.8v4.8h6.7v3.6h-6.7V88h7.8
v3.6H183.1z M200,91.5V70.9h5.3l5,8.5l5-8.5h5.3v20.7h-4.2V76.8l-4.6,7.6h-3l-4.6-7.6v14.7H200z M226.3,91.5V70.9h12v3.6h-7.8v4.8
h6.7v3.6h-6.7V88h7.8v3.6H226.3z M243.2,91.5V70.9h5.4l9.2,14.8V70.9h4.2v20.7h-5.4l-9.2-14.8v14.8H243.2z M265.7,74.7v-3.8h16.9
v3.8h-6.4v16.8H272V74.7H265.7z"
fill="#ffffff"
/>
<g id="Marianne">
<path
id="Fond_2_"
class="st0"
d="M63.6,53.4c0.3-0.3,0.6-0.6,0.9-1h0c0.6-0.6,1.1-1.3,1.8-1.8c0.2-0.2,0.4-0.3,0.6-0.5
c0.1-0.1,0.1-0.2,0.1-0.2c-0.3,0.1-0.4,0.3-0.7,0.4c-0.1,0-0.1-0.1-0.1-0.1c0.2-0.1,0.4-0.3,0.6-0.4c0,0,0,0,0,0
c-0.1,0-0.1-0.1-0.1-0.1c-0.7-0.1-1.2,0.4-1.7,0.8c-0.1,0.1-0.2-0.1-0.3-0.1c-0.8,0.3-1.4,1-2.2,1.3v-0.1c-0.3,0.1-0.6,0.3-1,0.4
c-0.5,0.1-0.9,0.1-1.3,0.1c-0.6,0.1-1.3,0.2-1.9,0.3c0,0,0,0-0.1,0c-0.3,0.1-0.7,0.2-1,0.4c0,0,0,0,0,0c0,0-0.1,0.1-0.1,0.1
c-0.1,0.1-0.2,0.2-0.4,0.3c-0.3,0.2-0.6,0.5-0.9,0.7c0,0-0.1,0-0.1,0c-0.3,0.3-0.6,0.6-0.9,0.8c0,0-0.1,0-0.2,0c0,0,0,0,0,0
c0,0,0,0,0-0.1c0-0.1,0.1-0.2,0.1-0.2c0.1-0.1,0.1-0.2,0.2-0.2c0.1-0.1,0.1-0.2,0.2-0.3c0,0,0-0.1,0-0.1c0,0,0,0-0.1,0
c0.3-0.3,0.6-0.5,0.9-0.7v0c0,0-0.1,0-0.1-0.1c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0.1-0.2,0.1-0.3,0.2
c-0.1,0.1-0.2,0.4-0.4,0.3c0,0-0.1,0-0.1,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
c0,0,0,0,0,0c0,0,0,0,0-0.1c0,0,0,0,0,0c0,0,0-0.1,0.1-0.1c0,0,0,0,0-0.1c0,0,0-0.1,0.1-0.1c0,0,0-0.1,0-0.1
c0.1-0.1,0.2-0.2,0.4-0.3h0c0.2-0.1,0.4-0.2,0.6-0.3c0,0,0.1-0.1,0.1-0.1c-0.3,0.1-0.6,0.2-0.9,0.4c0,0-0.1,0-0.1,0
c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0.1-0.1,0.2-0.2,0.3-0.3c0.1,0,0.1,0,0.1,0.1c1.7-1.3,4.1-1,6.1-1.7c0.2-0.1,0.3-0.2,0.5-0.3
c0.3-0.1,0.5-0.4,0.8-0.5c0.4-0.3,0.7-0.7,0.9-1.2c0-0.1-0.1-0.1-0.1-0.1c-0.7,0.8-1.5,1.3-2.4,1.8c-1.1,0.6-2.4,0.5-3.5,0.6
c0.1-0.1,0.2-0.1,0.3-0.1c0-0.2,0.1-0.2,0.2-0.3H59c0.1,0,0.1-0.1,0.1-0.1c0.1,0,0.3-0.1,0.2-0.1c-0.2-0.2-0.5,0.2-0.8,0
c0.1-0.1,0.1-0.3,0.2-0.3h0.2c0-0.1,0.1-0.2,0.1-0.2c0.8-0.5,1.6-0.9,2.3-1.3c-0.2,0-0.3,0.2-0.4,0.1c0.1,0,0-0.2,0.1-0.2
c0.6-0.2,1.1-0.5,1.7-0.7c-0.2,0-0.4,0.2-0.6,0c0.1-0.1,0.2-0.2,0.3-0.2v-0.2c0-0.1,0.1-0.1,0.1-0.1c-0.1,0-0.1-0.1-0.1-0.1
c0.1-0.1,0.2-0.1,0.3-0.2c-0.1,0-0.2,0-0.2-0.1c0.2-0.2,0.4-0.3,0.7-0.3c-0.1-0.1-0.2,0-0.2-0.1c0-0.1,0.1-0.1,0.1-0.1H63
c-0.1-0.1-0.1-0.2-0.1-0.2c0.3-0.4,0.3-0.9,0.5-1.3c-0.1,0-0.1,0-0.1-0.1c-0.5,0.6-1.4,0.8-2.2,1h-0.3c-0.3,0.1-0.6,0.1-0.9-0.1
c-0.2-0.1-0.3-0.3-0.5-0.4c-0.4-0.3-0.9-0.5-1.3-0.6c-1.3-0.4-2.7-0.6-4.1-0.6c0.6-0.3,1.2-0.3,1.9-0.5c0.9-0.3,1.8-0.6,2.7-0.5
c-0.2-0.1-0.4,0-0.5,0c-0.8-0.1-1.5,0.2-2.3,0.3c-0.5,0.1-1,0.3-1.6,0.4c-0.3,0.1-0.5,0.4-0.9,0.4v-0.2c0.5-0.6,1.2-1.3,2-1.3
c1-0.2,1.9,0,2.8,0.1c0.7,0.1,1.3,0.2,2,0.4c0.3,0,0.3,0.4,0.5,0.5c0.3,0.1,0.6,0,1,0.2c0-0.1-0.1-0.2,0-0.3
c0.2-0.2,0.5,0.1,0.7-0.1c0.4-0.3-0.4-0.7-0.6-1.1c0-0.1,0.1-0.1,0.1-0.1c0.4,0.4,0.7,0.8,1.3,1.1c0.3,0.1,0.9,0.3,0.8-0.1
c-0.3-0.6-0.8-1.1-1.2-1.6v-0.2c-0.1,0-0.1-0.1-0.2-0.1v-0.2c-0.2-0.1-0.2-0.3-0.3-0.5c-0.2-0.3-0.1-0.6-0.2-1
C62.1,39.3,62,39,62,38.7c-0.2-0.9-0.4-1.7-0.5-2.6c-0.1-1,0.6-1.8,1.1-2.7c0.4-0.6,0.8-1.3,1.5-1.7c0.2-0.6,0.6-1.2,1-1.7
c0.4-0.5,1.1-0.8,1.7-1.1c0.7-0.3,1.4-0.5,1.4-0.5l10.7,0c0,0,0.1,0,0.3,0.1c0.2,0.1,0.6,0.3,0.8,0.4c0.4,0.2,0.8,0.5,1,0.9
c0.1,0.2,0.3,0.5,0.2,0.7c-0.1,0.3-0.2,0.7-0.4,0.8c-0.3,0.2-0.7,0.2-1.1,0.1c-0.2,0-0.4-0.1-0.6-0.1c0.8,0.3,1.6,0.7,2.1,1.4
c0.1,0.1,0.3,0.2,0.5,0.2c0.1,0,0.1,0.1,0.1,0.2c-0.1,0.1-0.2,0.2-0.2,0.3h0.2c0.3-0.1,0.2-0.6,0.6-0.5c0.3,0.2,0.4,0.5,0.2,0.8
c-0.2,0.2-0.4,0.4-0.6,0.5c-0.1,0.1-0.1,0.3,0,0.4c0.2,0.2,0.2,0.4,0.3,0.6c0.2,0.4,0.2,0.8,0.4,1.2c0.2,0.8,0.4,1.6,0.4,2.4
c0,0.4-0.2,0.8-0.1,1.2c0.1,0.4,0.4,0.8,0.6,1.1c0.2,0.3,0.4,0.5,0.6,0.9c0.3,0.5,0.9,1.1,0.6,1.7c-0.2,0.4-0.8,0.3-1.1,0.5
c-0.3,0.3-0.1,0.7,0.1,1c0.3,0.5-0.3,0.8-0.7,1c0.1,0.2,0.3,0.1,0.4,0.2c0.1,0.3,0.3,0.4,0.2,0.7c-0.2,0.3-0.9,0.5-0.5,1
c0.2,0.4,0.1,0.8-0.1,1.2c-0.2,0.5-0.6,0.7-1,0.8c-0.3,0.1-0.7,0.1-1,0.1c-0.1-0.1-0.2-0.1-0.3-0.1c-0.9-0.1-1.8-0.4-2.7-0.4
c-0.3,0.1-0.5,0.1-0.7,0.2c-0.2,0.2-0.5,0.4-0.6,0.6c0,0,0,0,0,0c0,0-0.1,0.1-0.1,0.1c0,0,0,0.1-0.1,0.1c0,0,0,0,0,0.1
c-0.2,0.2-0.3,0.4-0.4,0.6c0,0,0,0,0,0c0,0,0,0.1,0,0.1c-0.2,0.3-0.3,0.7-0.4,1c-0.4,1.2-0.2,2.3,0.1,2.5c0.1,0.1,1.8,0.6,2.9,1.1
c0.6,0.2,0.9,0.4,1.3,0.6l-22,0c1-0.7,2-1.1,3.5-1.8C61.6,54.6,63.1,53.9,63.6,53.4 M55.4,49.5c-0.1,0-0.3,0.1-0.3-0.1
c0.1-0.3,0.4-0.3,0.6-0.4c0.1-0.1,0.3-0.2,0.4-0.1c0.1,0.2,0.3,0.1,0.4,0.2C56.2,49.5,55.8,49.4,55.4,49.5 M47.2,48.3
c0,0-0.1-0.1-0.1-0.1c0.7-0.9,1.2-1.8,1.7-2.7c0.7-0.4,1.3-0.9,1.8-1.5c0.9-1,1.9-1.8,3-2.4c0.4-0.2,1-0.1,1.4,0.1
c-0.2,0.2-0.4,0.2-0.6,0.3c-0.1,0-0.1,0-0.2-0.1c0.1-0.1,0.1-0.1,0.1-0.2c-0.5,0.6-1.3,0.9-1.7,1.6c-0.3,0.5-0.5,1.2-1.2,1.4
c-0.2,0.1,0.1-0.2-0.1-0.1C49.6,45.7,48.5,46.9,47.2,48.3 M51.6,44.8c-0.1,0.1-0.1,0.1-0.2,0.2c-0.1,0.1-0.1,0.2-0.2,0.2
c-0.1,0-0.1,0-0.1-0.1c0.1-0.2,0.2-0.4,0.4-0.5C51.6,44.7,51.6,44.8,51.6,44.8 M54.1,52.8c0,0.1-0.1,0.1-0.1,0.2
c0.1,0,0.1,0,0.1,0.1c-0.1,0.1-0.2,0.2-0.4,0.3c0,0,0,0-0.1,0c-0.1,0.1-0.1,0.1-0.2,0.2c-0.1,0.1-0.4,0-0.3-0.1
c0.1-0.1,0.3-0.3,0.4-0.4c0.1-0.1,0.2-0.1,0.2-0.2c0,0,0.1-0.1,0.1-0.1C53.9,52.8,54.2,52.7,54.1,52.8 M53.2,52.4
C53.2,52.4,53.1,52.4,53.2,52.4c-0.2,0.2-0.4,0.3-0.6,0.4c-0.2,0.1-0.5,0.2-0.7,0.3c0,0,0,0,0,0c0,0-0.1,0-0.1,0
c-0.2,0.1-0.4,0.3-0.5,0.4c0,0,0,0-0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1,0.1-0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0
c0,0-0.1,0.1-0.1,0.1c0,0,0,0.1-0.1,0.1c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0
c-0.1,0.1-0.1,0.1-0.2,0.2c-0.1,0.1-0.2,0.2-0.3,0.3c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0.1
l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0-0.1,0-0.1l0,0c0,0,0,0,0,0c0.1-0.1,0.1-0.1,0.2-0.2c0,0,0,0,0,0c0,0,0,0,0.1-0.1
c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0c0.1-0.1,0.1-0.2,0.2-0.2c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1-0.1,0.1-0.1c0,0,0-0.1,0.1-0.1
c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0-0.1,0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0-0.1c0,0,0,0,0,0c0,0,0-0.1,0-0.1
c0,0,0,0,0,0c0.1-0.1,0.1-0.2,0.2-0.3c0,0,0,0,0,0c-0.1,0-0.1,0.1-0.2,0.2c-0.1,0-0.2,0-0.1-0.1c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0
c0.1-0.1,0.2-0.2,0.3-0.3c0.1,0,0.1-0.1,0.2-0.1c0,0,0,0,0,0c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0c0.5-0.5,1.4-0.5,2.1-0.8
c0.3-0.1,0.6,0.1,0.9,0c0.2,0,0.3,0,0.5,0.1C54,51.8,53.6,52.1,53.2,52.4 M54.3,48.7c-0.1-0.1,0.2,0,0.2-0.1H54
c-0.1,0-0.1-0.1-0.1-0.1c-0.3,0.1-0.6,0.2-0.9,0.2c-0.4,0.1-0.7,0.4-1.1,0.5c-0.6,0.2-1.1,0.7-1.7,0.9c-0.1,0-0.1-0.1-0.1-0.1
c0.1-0.2,0.3-0.2,0.4-0.4c0-0.1,0-0.1-0.1-0.1c0.4-0.6,1-0.9,1.6-1.4v-0.2c0.2-0.2,0.4-0.3,0.5-0.6c0.1-0.2,0.3-0.4,0.5-0.5
c-0.1-0.1-0.2-0.1-0.2-0.2c-0.2,0-0.4,0.1-0.6-0.1c0.1-0.1,0.2-0.2,0.3-0.2c0,0-0.1,0-0.1-0.1c-0.1-0.1,0.1-0.2,0.3-0.3
c0.2-0.1,0.5-0.1,0.6-0.2c-0.4-0.1-0.8,0.1-1.2-0.1c0.3-0.7,0.7-1.3,1.3-1.6c0.1,0,0.2,0,0.2,0.1c0,0.3-0.2,0.5-0.4,0.5
c0.4,0.1,0.9,0.1,1.3,0.3c-0.1,0.1-0.2,0.1-0.2,0.1c0.3,0.2,0.6,0.1,0.9,0.3c-0.2,0.2-0.3,0-0.5,0c1.7,0.5,3.4,0.9,4.8,1.9
c-1.2,0.6-2.4,0.9-3.7,1.1c-0.2,0-0.3,0-0.4-0.1c0,0.1,0,0.2-0.1,0.2c-0.2,0-0.4,0-0.5,0.1C54.7,48.8,54.4,48.8,54.3,48.7"
/>
<path
id="Rouge_1_"
class="st1"
d="M63.6,53.4c0.3-0.3,0.6-0.6,0.9-1h0c0.6-0.6,1.1-1.3,1.8-1.8c0.2-0.2,0.4-0.3,0.6-0.5
c0.1-0.1,0.1-0.2,0.1-0.2c-0.3,0.1-0.4,0.3-0.7,0.4c-0.1,0-0.1-0.1-0.1-0.1c0.2-0.1,0.4-0.3,0.6-0.4c0,0,0,0,0,0
c-0.1,0-0.1-0.1-0.1-0.1c-0.7-0.1-1.2,0.4-1.7,0.8c-0.1,0.1-0.2-0.1-0.3-0.1c-0.8,0.3-1.4,1-2.2,1.3v-0.1c-0.3,0.1-0.6,0.3-1,0.4
c-0.5,0.1-0.9,0.1-1.3,0.1c-0.6,0.1-1.3,0.2-1.9,0.3c0,0,0,0-0.1,0c-0.3,0.1-0.7,0.2-1,0.4c0,0,0,0,0,0c0,0-0.1,0.1-0.1,0.1
c-0.1,0.1-0.2,0.2-0.4,0.3c-0.3,0.2-0.6,0.5-0.9,0.7c0,0-0.1,0-0.1,0c-0.3,0.3-0.6,0.6-0.9,0.8c0,0-0.1,0-0.2,0c0,0,0,0,0,0
c0,0,0,0,0-0.1c0-0.1,0.1-0.2,0.1-0.2c0.1-0.1,0.1-0.2,0.2-0.2c0.1-0.1,0.1-0.2,0.2-0.3c0,0,0-0.1,0-0.1c0,0,0,0-0.1,0
c0.3-0.3,0.6-0.5,0.9-0.7v0c0,0-0.1,0-0.1-0.1c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0.1-0.2,0.1-0.3,0.2
c-0.1,0.1-0.2,0.4-0.4,0.3c0,0-0.1,0-0.1,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
c0,0,0,0,0,0c0,0,0,0,0-0.1c0,0,0,0,0,0c0,0,0-0.1,0.1-0.1c0,0,0,0,0-0.1c0,0,0-0.1,0.1-0.1c0,0,0-0.1,0-0.1
c0.1-0.1,0.2-0.2,0.4-0.3h0c0.2-0.1,0.4-0.2,0.6-0.3c0,0,0.1-0.1,0.1-0.1c-0.3,0.1-0.6,0.2-0.9,0.4c0,0-0.1,0-0.1,0
c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0.1-0.1,0.2-0.2,0.3-0.3c0.1,0,0.1,0,0.1,0.1c1.7-1.3,4.1-1,6.1-1.7c0.2-0.1,0.3-0.2,0.5-0.3
c0.3-0.1,0.5-0.4,0.8-0.5c0.4-0.3,0.7-0.7,0.9-1.2c0-0.1-0.1-0.1-0.1-0.1c-0.7,0.8-1.5,1.3-2.4,1.8c-1.1,0.6-2.4,0.5-3.5,0.6
c0.1-0.1,0.2-0.1,0.3-0.1c0-0.2,0.1-0.2,0.2-0.3H59c0.1,0,0.1-0.1,0.1-0.1c0.1,0,0.3-0.1,0.2-0.1c-0.2-0.2-0.5,0.2-0.8,0
c0.1-0.1,0.1-0.3,0.2-0.3h0.2c0-0.1,0.1-0.2,0.1-0.2c0.8-0.5,1.6-0.9,2.3-1.3c-0.2,0-0.3,0.2-0.4,0.1c0.1,0,0-0.2,0.1-0.2
c0.6-0.2,1.1-0.5,1.7-0.7c-0.2,0-0.4,0.2-0.6,0c0.1-0.1,0.2-0.2,0.3-0.2v-0.2c0-0.1,0.1-0.1,0.1-0.1c-0.1,0-0.1-0.1-0.1-0.1
c0.1-0.1,0.2-0.1,0.3-0.2c-0.1,0-0.2,0-0.2-0.1c0.2-0.2,0.4-0.3,0.7-0.3c-0.1-0.1-0.2,0-0.2-0.1c0-0.1,0.1-0.1,0.1-0.1H63
c-0.1-0.1-0.1-0.2-0.1-0.2c0.3-0.4,0.3-0.9,0.5-1.3c-0.1,0-0.1,0-0.1-0.1c-0.5,0.6-1.4,0.8-2.2,1h-0.3c-0.3,0.1-0.6,0.1-0.9-0.1
c-0.2-0.1-0.3-0.3-0.5-0.4c-0.4-0.3-0.9-0.5-1.3-0.6c-1.3-0.4-2.7-0.6-4.1-0.6c0.6-0.3,1.2-0.3,1.9-0.5c0.9-0.3,1.8-0.6,2.7-0.5
c-0.2-0.1-0.4,0-0.5,0c-0.8-0.1-1.5,0.2-2.3,0.3c-0.5,0.1-1,0.3-1.6,0.4c-0.3,0.1-0.5,0.4-0.9,0.4V44c0.5-0.6,1.2-1.3,2-1.3
c1-0.2,1.9,0,2.8,0.1c0.7,0.1,1.3,0.2,2,0.4c0.3,0,0.3,0.4,0.5,0.5c0.3,0.1,0.6,0,1,0.2c0-0.1-0.1-0.2,0-0.3
c0.2-0.2,0.5,0.1,0.7-0.1c0.4-0.3-0.4-0.7-0.6-1.1c0-0.1,0.1-0.1,0.1-0.1c0.4,0.4,0.7,0.8,1.3,1.1c0.3,0.1,0.9,0.3,0.8-0.1
c-0.3-0.6-0.8-1.1-1.2-1.6v-0.2c-0.1,0-0.1-0.1-0.2-0.1v-0.2c-0.2-0.1-0.2-0.3-0.3-0.5c-0.2-0.3-0.1-0.6-0.2-1
C62.1,39.3,62,39,62,38.7c-0.2-0.9-0.4-1.7-0.5-2.6c-0.1-1,0.6-1.8,1.1-2.7c0.4-0.6,0.8-1.3,1.5-1.7c0.2-0.6,0.6-1.2,1-1.7
c0.4-0.5,1.1-0.8,1.7-1.1c0.7-0.3,1.4-0.5,1.4-0.5H31.4v28.3h26c1-0.7,2-1.1,3.5-1.8C61.6,54.6,63.1,53.9,63.6,53.4 M55.4,49.5
c-0.1,0-0.3,0.1-0.3-0.1c0.1-0.3,0.4-0.3,0.6-0.4c0.1-0.1,0.3-0.2,0.4-0.1c0.1,0.2,0.3,0.1,0.4,0.2C56.2,49.5,55.8,49.4,55.4,49.5
M47.2,48.3c0,0-0.1-0.1-0.1-0.1c0.7-0.9,1.2-1.8,1.7-2.7c0.7-0.4,1.3-0.9,1.8-1.5c0.9-1,1.9-1.8,3-2.4c0.4-0.2,1-0.1,1.4,0.1
c-0.2,0.2-0.4,0.2-0.6,0.3c-0.1,0-0.1,0-0.2-0.1c0.1-0.1,0.1-0.1,0.1-0.2c-0.5,0.6-1.3,0.9-1.7,1.6c-0.3,0.5-0.5,1.2-1.2,1.4
c-0.2,0.1,0.1-0.2-0.1-0.1C49.7,45.7,48.5,46.9,47.2,48.3 M51.6,44.8c-0.1,0.1-0.1,0.1-0.2,0.2c-0.1,0.1-0.1,0.2-0.2,0.2
c-0.1,0-0.1,0-0.1-0.1c0.1-0.2,0.2-0.4,0.4-0.5C51.6,44.7,51.6,44.8,51.6,44.8 M54.1,52.8c0,0.1-0.1,0.1-0.1,0.2
c0.1,0,0.1,0,0.1,0.1c-0.1,0.1-0.2,0.2-0.4,0.3c0,0,0,0-0.1,0c-0.1,0.1-0.1,0.1-0.2,0.2c-0.1,0.1-0.4,0-0.3-0.1
c0.1-0.1,0.3-0.3,0.4-0.4c0.1-0.1,0.2-0.1,0.2-0.2c0,0,0.1-0.1,0.1-0.1C53.9,52.8,54.2,52.7,54.1,52.8 M53.2,52.4
C53.2,52.4,53.2,52.4,53.2,52.4c-0.2,0.2-0.4,0.3-0.6,0.4c-0.2,0.1-0.5,0.2-0.7,0.3c0,0,0,0,0,0c0,0-0.1,0-0.1,0
c-0.2,0.1-0.4,0.3-0.5,0.4c0,0,0,0-0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1,0.1-0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0
c0,0-0.1,0.1-0.1,0.1c0,0,0,0.1-0.1,0.1c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0
c-0.1,0.1-0.1,0.1-0.2,0.2c-0.1,0.1-0.2,0.2-0.3,0.3c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0.1
l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0-0.1,0-0.1l0,0c0,0,0,0,0,0c0.1-0.1,0.1-0.1,0.2-0.2c0,0,0,0,0,0c0,0,0,0,0.1-0.1
c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0c0.1-0.1,0.1-0.2,0.2-0.2c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1-0.1,0.1-0.1c0,0,0-0.1,0.1-0.1
c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0-0.1,0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0-0.1c0,0,0,0,0,0c0,0,0-0.1,0-0.1
c0,0,0,0,0,0c0.1-0.1,0.1-0.2,0.2-0.3c0,0,0,0,0,0c-0.1,0-0.1,0.1-0.2,0.2c-0.1,0-0.2,0-0.1-0.1c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0
c0.1-0.1,0.2-0.2,0.3-0.3c0.1,0,0.1-0.1,0.2-0.1c0,0,0,0,0,0c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0c0.5-0.5,1.4-0.5,2.1-0.8
c0.3-0.1,0.6,0.1,0.9,0c0.2,0,0.3,0,0.5,0.1C54,51.8,53.6,52.1,53.2,52.4 M54.3,48.7c-0.1-0.1,0.2,0,0.2-0.1H54
c-0.1,0-0.1-0.1-0.1-0.1c-0.3,0.1-0.6,0.2-0.9,0.2c-0.4,0.1-0.7,0.4-1.1,0.5c-0.6,0.2-1.1,0.7-1.7,0.9c-0.1,0-0.1-0.1-0.1-0.1
c0.1-0.2,0.3-0.2,0.4-0.4c0-0.1,0-0.1-0.1-0.1c0.4-0.6,1-0.9,1.6-1.4v-0.2c0.2-0.2,0.4-0.3,0.5-0.6c0.1-0.2,0.3-0.4,0.5-0.5
c-0.1-0.1-0.2-0.1-0.2-0.2c-0.2,0-0.4,0.1-0.6-0.1c0.1-0.1,0.2-0.2,0.3-0.2c0,0-0.1,0-0.1-0.1c-0.1-0.1,0.1-0.2,0.3-0.3
c0.2-0.1,0.5-0.1,0.6-0.2c-0.4-0.1-0.8,0.1-1.2-0.1c0.3-0.7,0.7-1.3,1.3-1.6c0.1,0,0.2,0,0.2,0.1c0,0.3-0.2,0.5-0.4,0.5
c0.4,0.1,0.9,0.1,1.3,0.3c-0.1,0.1-0.2,0.1-0.2,0.1c0.3,0.2,0.6,0.1,0.9,0.3c-0.2,0.2-0.3,0-0.5,0c1.7,0.5,3.4,0.9,4.8,1.9
c-1.2,0.6-2.4,0.9-3.7,1.1c-0.2,0-0.3,0-0.4-0.1c0,0.1,0,0.2-0.1,0.2c-0.2,0-0.4,0-0.5,0.1C54.7,48.8,54.4,48.8,54.3,48.7"
/>
<path
id="Bleu_1_"
class="st2"
d="M109.3,28.4H78.9c0,0,0.1,0,0.3,0.1c0.2,0.1,0.6,0.3,0.8,0.4c0.4,0.2,0.8,0.5,1,0.9
c0.1,0.2,0.3,0.5,0.2,0.7c-0.1,0.3-0.2,0.7-0.4,0.8c-0.3,0.2-0.7,0.2-1.1,0.1c-0.2,0-0.4-0.1-0.6-0.1c0.8,0.3,1.6,0.7,2.1,1.4
c0.1,0.1,0.3,0.2,0.5,0.2c0.1,0,0.1,0.1,0.1,0.2c-0.1,0.1-0.2,0.2-0.2,0.3h0.2c0.3-0.1,0.2-0.6,0.6-0.5c0.3,0.2,0.4,0.5,0.2,0.8
c-0.2,0.2-0.4,0.4-0.6,0.5c-0.1,0.1-0.1,0.3,0,0.4c0.2,0.2,0.2,0.4,0.3,0.6c0.2,0.4,0.2,0.8,0.4,1.2c0.2,0.8,0.4,1.6,0.4,2.4
c0,0.4-0.2,0.8-0.1,1.2c0.1,0.4,0.4,0.8,0.6,1.1c0.2,0.3,0.4,0.5,0.6,0.9c0.3,0.5,0.9,1.1,0.6,1.7c-0.2,0.4-0.8,0.3-1.1,0.5
c-0.3,0.3-0.1,0.7,0.1,1c0.3,0.5-0.3,0.8-0.7,1c0.1,0.2,0.3,0.1,0.4,0.2c0.1,0.3,0.3,0.4,0.2,0.7c-0.2,0.3-0.9,0.5-0.5,1
c0.2,0.4,0.1,0.8-0.1,1.2c-0.2,0.5-0.6,0.7-1,0.8c-0.3,0.1-0.7,0.1-1,0.1c-0.1-0.1-0.2-0.1-0.3-0.1c-0.9-0.1-1.8-0.4-2.7-0.4
c-0.3,0.1-0.5,0.1-0.7,0.2c-0.2,0.2-0.5,0.4-0.6,0.6c0,0,0,0,0,0c0,0-0.1,0.1-0.1,0.1c0,0,0,0.1-0.1,0.1c0,0,0,0,0,0.1
c-0.2,0.2-0.3,0.4-0.4,0.6c0,0,0,0,0,0c0,0,0,0.1,0,0.1c-0.2,0.3-0.3,0.7-0.4,1c-0.4,1.2-0.2,2.3,0.1,2.5c0.1,0.1,1.8,0.6,2.9,1.1
c0.6,0.2,0.9,0.4,1.3,0.6h29.9V28.4z"
/>
<path
id="Yeux_1_"
class="st3"
d="M80.7,38.7c0.2,0.1,0.5,0.1,0.5,0.2c-0.1,0.4-0.7,0.5-1.1,1H80c-0.2,0.1-0.1,0.4-0.3,0.4
c-0.2-0.1-0.3,0-0.5,0.1c0.2,0.2,0.5,0.4,0.8,0.3c0.1,0,0.2,0.1,0.2,0.2c0,0,0.1,0,0.1-0.1c0.1,0,0.1,0,0.1,0.1V41
c-0.2,0.2-0.4,0.1-0.6,0.2c0.4,0.1,0.9,0.1,1.2,0c0.3-0.1,0-0.6,0.2-0.9c-0.1,0,0-0.2-0.1-0.2c0.1-0.1,0.2-0.3,0.3-0.3
c0.1,0,0.3-0.1,0.3-0.2c0-0.1-0.2-0.2-0.2-0.3c0.3-0.2,0.6-0.5,0.5-0.9c-0.1-0.2-0.5-0.2-0.8-0.3c-0.3-0.1-0.6,0-0.9,0.1
c-0.3,0-0.5,0.2-0.8,0.2c-0.4,0.1-0.7,0.3-1,0.5c0.4-0.2,0.8-0.2,1.2-0.3C80.1,38.7,80.3,38.6,80.7,38.7"
/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 23 KiB

@@ -0,0 +1,12 @@
<svg width="128" height="40" viewBox="0 0 128 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.2902 26.2256V26.6237C23.2902 28.3039 21.9405 29.6701 20.2806 29.6701H13.2164C11.5565 29.6701 10.2068 28.3039 10.2068 26.6237V19.4729C10.2068 17.7927 11.5565 16.4265 13.2164 16.4265H13.6097V26.2274H23.2921L23.2902 26.2256Z" fill="#927AFA"/>
<path d="M23.4214 23.8407H15.9639V15.4957C15.9639 12.6474 18.2534 10.3299 21.0673 10.3299H23.4232C26.6692 10.3299 29.3113 13.0044 29.3113 16.2901V17.8787C29.3113 21.1644 26.6692 23.8389 23.4232 23.8389L23.4214 23.8407ZM19.3649 20.3962H23.4214C24.7914 20.3962 25.9066 19.2673 25.9066 17.8806V16.2919C25.9066 14.9051 24.7914 13.7763 23.4214 13.7763H21.0654C20.1274 13.7763 19.3649 14.5482 19.3649 15.4976V20.3981V20.3962Z" fill="#571EFA"/>
<path d="M38.9254 16.9629C41.6673 16.9629 43.4195 19.0001 43.4195 21.4952C43.4195 23.9903 41.6673 26.0275 38.9254 26.0275C36.1835 26.0275 34.4128 23.9903 34.4128 21.4952C34.4128 19.0001 36.1651 16.9629 38.9254 16.9629ZM38.9254 24.921C41.0247 24.921 42.1362 23.3399 42.1362 21.4952C42.1362 19.6505 41.0247 18.0693 38.9254 18.0693C36.8261 18.0693 35.6979 19.6505 35.6979 21.4952C35.6979 23.3399 36.8076 24.921 38.9254 24.921Z" fill="black"/>
<path d="M45.0867 17.1386H46.3717V19.3515C46.9792 17.8936 48.2643 16.9629 50.1384 16.9629C52.5516 16.9629 54.1819 18.8076 54.1819 21.512C54.1819 24.2164 52.5516 26.0275 50.1384 26.0275C48.2292 26.0275 46.9626 25.0612 46.3717 23.6389V29.0496H45.0867V17.1386ZM49.7377 24.921C51.4733 24.921 52.8968 23.8314 52.8968 21.512C52.8968 19.1926 51.4733 18.0693 49.7377 18.0693C48.0021 18.0693 46.4234 19.2113 46.4234 21.512C46.4234 23.8127 48.0206 24.921 49.7377 24.921Z" fill="black"/>
<path d="M59.6306 16.9629C62.0955 16.9629 63.7443 18.5272 63.7443 21.1961V21.7232H56.8204C56.8721 23.5511 57.7916 24.9547 59.7007 24.9547C61.2794 24.9547 62.2007 24.1118 62.5128 22.811H63.7794C63.4157 24.3566 62.3743 26.0256 59.7358 26.0256C56.9423 26.0256 55.5889 23.9174 55.5889 21.37C55.5889 18.559 57.2026 16.961 59.6324 16.961L59.6306 16.9629ZM62.4758 20.7046C62.354 19.0001 61.2258 18.0338 59.6121 18.0338C58.1018 18.0338 56.9736 19.0169 56.8352 20.7046H62.4758Z" fill="black"/>
<path d="M65.5483 17.1386H66.8334V19.5626C67.3024 17.946 68.5173 16.9629 70.408 16.9629C72.2987 16.9629 73.497 18.2282 73.497 20.5121V25.8518H72.1953V20.6168C72.1953 18.8076 71.5011 18.0693 69.9741 18.0693C68.0299 18.0693 66.8334 19.7215 66.8334 22.0577V25.8518H65.5483V17.1386Z" fill="black"/>
<path d="M75.6665 13.466H80.1624C81.4346 13.466 82.5572 13.6249 83.5302 13.9408C84.5014 14.2566 85.2972 14.8715 85.9157 15.7854C86.5343 16.6994 86.8445 17.9871 86.8445 19.6505C86.8445 21.3139 86.5343 22.6203 85.9157 23.5324C85.2972 24.4463 84.5014 25.0612 83.5302 25.3771C82.5572 25.6929 81.4364 25.8518 80.1624 25.8518H75.6665V13.466ZM80.1624 23.3754C81.0413 23.3754 81.7448 23.3044 82.271 23.1642C82.7972 23.024 83.2256 22.6876 83.5561 22.1531C83.8866 21.6204 84.0509 20.785 84.0509 19.6505C84.0509 18.516 83.8829 17.6955 83.5487 17.1554C83.2126 16.6171 82.7843 16.277 82.2636 16.1368C81.7429 15.9966 81.0432 15.9256 80.1643 15.9256H78.4453V23.3754H80.1643H80.1624Z" fill="black"/>
<path d="M88.6575 18.9384C89.0453 18.2525 89.5826 17.7292 90.2713 17.3666C90.96 17.004 91.7502 16.8227 92.6402 16.8227C94.0988 16.8227 95.238 17.2152 96.0597 18.0002C96.8813 18.7852 97.2912 19.8692 97.2912 21.2503V21.9886H90.731C90.7772 22.6203 90.9747 23.1156 91.3219 23.4726C91.669 23.8295 92.1435 24.009 92.7454 24.009C93.2661 24.009 93.6963 23.8987 94.0379 23.6744C94.3795 23.452 94.5955 23.1362 94.6897 22.725H97.3097C97.1712 23.7081 96.704 24.5061 95.912 25.1135C95.1199 25.7228 94.0693 26.0275 92.762 26.0275C91.8019 26.0275 90.9692 25.8312 90.262 25.4387C89.5567 25.0463 89.0157 24.4986 88.6391 23.7959C88.2624 23.0932 88.0759 22.2858 88.0759 21.3718C88.0759 20.4579 88.2698 19.6243 88.6575 18.9384ZM94.7229 20.5457C94.6767 19.9832 94.4718 19.5496 94.1062 19.2449C93.7425 18.9403 93.2698 18.7889 92.6919 18.7889C92.114 18.7889 91.6653 18.9422 91.3126 19.2449C90.96 19.5496 90.7606 19.9832 90.7144 20.5457H94.7229Z" fill="black"/>
<path d="M99.6342 25.1659C98.8698 24.5921 98.4875 23.7492 98.4875 22.6352H101.126C101.126 23.1399 101.283 23.5118 101.595 23.751C101.907 23.9921 102.387 24.1117 103.035 24.1117C103.51 24.1117 103.853 24.0501 104.067 23.9267C104.281 23.8034 104.389 23.6071 104.389 23.338C104.389 23.1511 104.326 22.9978 104.198 22.882C104.071 22.7642 103.868 22.6652 103.591 22.5829L100.917 21.8447C100.327 21.6933 99.8188 21.4204 99.3904 21.0279C98.9621 20.6354 98.7479 20.0934 98.7479 19.4019C98.7479 18.5702 99.0858 17.9329 99.7634 17.4862C100.441 17.0414 101.386 16.819 102.601 16.819C103.944 16.819 104.99 17.0918 105.742 17.6357C106.493 18.1796 106.87 18.9683 106.87 19.9981H104.232C104.232 19.144 103.694 18.716 102.618 18.716C102.236 18.716 101.935 18.7814 101.715 18.9085C101.495 19.0375 101.385 19.2075 101.385 19.4187C101.385 19.7813 101.702 20.0448 102.339 20.2093L104.387 20.7195C105.184 20.9195 105.825 21.226 106.305 21.6428C106.785 22.0577 107.025 22.6409 107.025 23.3903C107.025 24.2463 106.687 24.8986 106.01 25.349C105.332 25.7994 104.329 26.0256 102.998 26.0256C101.517 26.0256 100.395 25.7378 99.6305 25.164L99.6342 25.1659Z" fill="black"/>
<path d="M108.486 12.939H111.124V20.4224L114.612 16.9965H117.84L113.484 21.3008L117.823 25.8518H114.612L111.124 22.1269V25.8518H108.486V12.939Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -1,7 +1,6 @@
import fetchMock from 'fetch-mock';
import { fetchAPI } from '@/api';
import { useAuthStore } from '@/core/auth';
describe('fetchAPI', () => {
beforeEach(() => {
@@ -30,19 +29,6 @@ describe('fetchAPI', () => {
});
});
it('logout if 401 response', async () => {
const logoutMock = jest.fn();
jest
.spyOn(useAuthStore.getState(), 'logout')
.mockImplementation(logoutMock);
fetchMock.mock('http://test.jest/api/v1.0/some/url', 401);
await fetchAPI('some/url');
expect(logoutMock).toHaveBeenCalled();
});
it('check the versionning', () => {
fetchMock.mock('http://test.jest/api/v2.0/some/url', 200);
+17 -23
View File
@@ -1,40 +1,34 @@
import { baseApiUrl, useAuthStore } from '@/core';
import { baseApiUrl } from '@/core';
/**
* Retrieves the CSRF token from the document's cookies.
*
* @returns {string|null} The CSRF token if found in the cookies, or null if not present.
*/
function getCSRFToken() {
return document.cookie
.split(';')
.filter((cookie) => cookie.trim().startsWith('csrftoken='))
.map((cookie) => cookie.split('=')[1])
.pop();
import { getCSRFToken } from './utils';
interface FetchAPIInit extends RequestInit {
withoutContentType?: boolean;
}
export const fetchAPI = async (
input: string,
init?: RequestInit,
init?: FetchAPIInit,
apiVersion = '1.0',
) => {
const apiUrl = `${baseApiUrl(apiVersion)}${input}`;
const csrfToken = getCSRFToken();
const headers = {
'Content-Type': 'application/json',
...init?.headers,
...(csrfToken && { 'X-CSRFToken': csrfToken }),
};
if (init?.withoutContentType) {
delete headers?.['Content-Type' as keyof typeof headers];
}
const response = await fetch(apiUrl, {
...init,
credentials: 'include',
headers: {
...init?.headers,
'Content-Type': 'application/json',
...(csrfToken && { 'X-CSRFToken': csrfToken }),
},
headers,
});
if (response.status === 401) {
const { logout } = useAuthStore.getState();
logout();
}
return response;
};
@@ -16,3 +16,14 @@ export const errorCauses = async (response: Response, data?: unknown) => {
data,
};
};
/**
* Retrieves the CSRF token from the document's cookies.
*/
export function getCSRFToken() {
return document.cookie
.split(';')
.filter((cookie) => cookie.trim().startsWith('csrftoken='))
.map((cookie) => cookie.split('=')[1])
.pop();
}
@@ -1,77 +1,18 @@
@font-face {
font-family: Marianne;
src: url('Marianne-Thin.woff') format('truetype');
font-weight: 100;
}
@font-face {
font-family: Marianne;
src: url('Marianne-Thin_Italic.woff') format('truetype');
font-weight: 100;
font-style: italic;
}
@font-face {
font-family: Marianne;
src: url('Marianne-Light.woff') format('truetype');
font-weight: 300;
}
@font-face {
font-family: Marianne;
src: url('Marianne-Light_Italic.woff') format('truetype');
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: Marianne;
src: url('Marianne-Regular.woff') format('truetype');
src: url('Marianne-Regular.woff2') format('woff2');
font-weight: 400;
}
@font-face {
font-family: Marianne;
src: url('Marianne-Regular_Italic.woff') format('truetype');
src: url('Marianne-Italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: Marianne;
src: url('Marianne-Medium.woff') format('truetype');
font-weight: 500;
}
@font-face {
font-family: Marianne;
src: url('Marianne-Medium_Italic.woff') format('truetype');
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: Marianne;
src: url('Marianne-Bold.woff') format('truetype');
src: url('Marianne-Bold.woff2') format('woff2');
font-weight: 700;
}
@font-face {
font-family: Marianne;
src: url('Marianne-Bold_Italic.woff') format('truetype');
font-weight: 700;
font-style: italic;
}
@font-face {
font-family: Marianne;
src: url('Marianne-ExtraBold.woff') format('truetype');
font-weight: 800;
}
@font-face {
font-family: Marianne;
src: url('Marianne-ExtraBold_Italic.woff') format('truetype');
font-weight: 800;
font-style: italic;
}
@@ -0,0 +1,30 @@
@font-face {
font-family: 'Open Sans';
src: url('OpenSans-Regular.woff2') format('woff2');
font-weight: 400;
}
@font-face {
font-family: 'Open Sans';
src: url('OpenSans-Italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'Open Sans';
src: url('OpenSans-Bold.woff2') format('woff2');
font-weight: 700;
}
@font-face {
font-family: 'Open Sans';
src: url('OpenSans-Semibold.woff2') format('woff2');
font-weight: 600;
}
@font-face {
font-family: 'Open Sans';
src: url('OpenSans-Bold.woff2') format('woff2');
font-weight: 500;
}
@@ -29,6 +29,7 @@ const BoxButton = forwardRef<HTMLDivElement, BoxType>(
border: none;
outline: none;
transition: all 0.2s ease-in-out;
font-family: inherit;
${$css || ''}
`}
{...props}
@@ -9,14 +9,14 @@ export const Card = ({
$css,
...props
}: PropsWithChildren<BoxType>) => {
const { colorsTokens } = useCunninghamTheme();
const { colorsTokens, componentTokens } = useCunninghamTheme();
return (
<Box
$background="white"
$background="var(--c--components--header--background)"
$radius="4px"
$css={`
box-shadow: 2px 2px 5px ${colorsTokens()['greyscale-300']};
box-shadow: 2px 2px 5px ${componentTokens()['card']['box-shadow']};
border: 1px solid ${colorsTokens()['card-border']};
${$css}
`}
@@ -13,6 +13,7 @@ export const IconBG = ({ iconName, ...textProps }: IconBGProps) => {
$isMaterialIcon
$size="36px"
$theme="primary"
$variation="700"
$background={colorsTokens()['primary-bg']}
$css={`border: 1px solid ${colorsTokens()['primary-200']}`}
$radius="12px"
@@ -39,6 +40,7 @@ export const IconOptions = ({ isOpen, ...props }: IconOptionsProps) => {
transition: all 0.3s ease-in-out;
transform: rotate(${isOpen ? '90' : '0'}deg);
`}
$color="var(--c--theme--colors--primary-600)"
>
more_vert
</Text>
@@ -1,21 +1,27 @@
import React, { useState } from 'react';
import React, { PropsWithChildren, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Card, IconBG, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Doc } from '@/features/docs/doc-management';
import { VersionList } from './VersionList';
interface PanelProps {
doc: Doc;
title: string;
setIsPanelOpen: (isOpen: boolean) => void;
}
export const Panel = ({ doc }: PanelProps) => {
export const Panel = ({
children,
title,
setIsPanelOpen,
}: PropsWithChildren<PanelProps>) => {
const { t } = useTranslation();
const { colorsTokens } = useCunninghamTheme();
const [isOpen, setIsOpen] = useState(true);
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
setIsOpen(true);
}, []);
const closedOverridingStyles = !isOpen && {
$width: '0',
@@ -43,31 +49,9 @@ export const Panel = ({ doc }: PanelProps) => {
`
}
`}
aria-label={t('Document version panel')}
aria-label={t('Document panel')}
{...closedOverridingStyles}
>
<IconBG
iconName="menu_open"
aria-label={
isOpen
? t('Close the document version panel')
: t('Open the document version panel')
}
$background="transparent"
$size="h2"
$zIndex={1}
$css={`
cursor: pointer;
left: ${isOpen ? '0' : '-3.3'}rem;
top: 0.1rem;
transform: rotate(${isOpen ? '180' : '0'}deg);
transition: ${transition};
user-select: none;
`}
$position="absolute"
onClick={() => setIsOpen(!isOpen)}
$radius="2px"
/>
<Box
$overflow="hidden"
$css={`
@@ -82,11 +66,35 @@ export const Panel = ({ doc }: PanelProps) => {
$justify="center"
$css={`border-top: 2px solid ${colorsTokens()['primary-600']};`}
>
<Text $weight="bold" $size="l" $theme="primary">
{t('VERSIONS')}
<IconBG
iconName="menu_open"
aria-label={isOpen ? t('Close the panel') : t('Open the panel')}
$background="transparent"
$size="h2"
$zIndex={1}
$css={`
cursor: pointer;
left: 0rem;
top: 0.1rem;
transition: ${transition};
transform: rotate(180deg);
opacity: ${isOpen ? '1' : '0'};
user-select: none;
`}
$position="absolute"
onClick={() => {
setIsOpen(false);
setTimeout(() => {
setIsPanelOpen(false);
}, 400);
}}
$radius="2px"
/>
<Text $weight="bold" $size="l" $theme="primary" $variation="900">
{title}
</Text>
</Box>
<VersionList doc={doc} />
{children}
</Box>
</Card>
);
@@ -4,7 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useCunninghamTheme } from '@/cunningham';
import '@/i18n/initI18n';
import { Auth } from './auth/Auth';
import { Auth } from './auth/';
/**
* QueryClient:
@@ -0,0 +1,34 @@
import { Button } from '@openfun/cunningham-react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '@/core/auth';
export const AccountDropdown = () => {
const { t } = useTranslation();
const { logout, authenticated, login } = useAuthStore();
if (!authenticated) {
return (
<Button
onClick={login}
color="primary-text"
icon={<span className="material-icons">login</span>}
aria-label={t('Login')}
>
{t('Login')}
</Button>
);
}
return (
<Button
onClick={logout}
color="primary-text"
icon={<span className="material-icons">logout</span>}
aria-label={t('Logout')}
>
{t('Logout')}
</Button>
);
};
@@ -1,18 +1,47 @@
import { Loader } from '@openfun/cunningham-react';
import { PropsWithChildren, useEffect } from 'react';
import { useRouter } from 'next/router';
import { PropsWithChildren, useEffect, useState } from 'react';
import { Box } from '@/components';
import { useAuthStore } from './useAuthStore';
/**
* TODO: Remove this restriction when we will have a homepage design for non-authenticated users.
*
* We define the paths that are not allowed without authentication.
* Actually, only the home page and the docs page are not allowed without authentication.
* When we will have a homepage design for non-authenticated users, we will remove this restriction to have
* the full website accessible without authentication.
*/
const regexpUrlsAuth = [/\/docs\/$/g, /^\/$/g];
export const Auth = ({ children }: PropsWithChildren) => {
const { authenticated, initAuth } = useAuthStore();
const { initAuth, initiated, authenticated, login } = useAuthStore();
const { asPath } = useRouter();
const [pathAllowed, setPathAllowed] = useState<boolean>(
!regexpUrlsAuth.some((regexp) => !!asPath.match(regexp)),
);
useEffect(() => {
initAuth();
}, [initAuth]);
if (!authenticated) {
useEffect(() => {
setPathAllowed(!regexpUrlsAuth.some((regexp) => !!asPath.match(regexp)));
}, [asPath]);
// We force to login except on allowed paths
useEffect(() => {
if (!initiated || authenticated || pathAllowed) {
return;
}
login();
}, [authenticated, pathAllowed, login, initiated]);
if ((!initiated && pathAllowed) || (!authenticated && !pathAllowed)) {
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
<Loader />
@@ -1,3 +1,4 @@
export * from './AccountDropdown';
export * from './api/types';
export * from './Auth';
export * from './useAuthStore';
export * from './api/types';
@@ -6,18 +6,22 @@ import { User, getMe } from './api';
import { PATH_AUTH_LOCAL_STORAGE } from './conf';
interface AuthStore {
initiated: boolean;
authenticated: boolean;
initAuth: () => void;
logout: () => void;
login: () => void;
userData?: User;
}
const initialState = {
initiated: false,
authenticated: false,
userData: undefined,
};
export const useAuthStore = create<AuthStore>((set) => ({
initiated: initialState.initiated,
authenticated: initialState.authenticated,
userData: initialState.userData,
@@ -34,20 +38,21 @@ export const useAuthStore = create<AuthStore>((set) => ({
set({ authenticated: true, userData: data });
})
.catch(() => {
// If we try to access a specific page and we are not authenticated
// we store the path in the local storage to redirect to it after login
if (window.location.pathname !== '/') {
localStorage.setItem(
PATH_AUTH_LOCAL_STORAGE,
window.location.pathname,
);
}
window.location.replace(new URL('authenticate/', baseApiUrl()).href);
.catch(() => {})
.finally(() => {
set({ initiated: true });
});
},
login: () => {
// If we try to access a specific page and we are not authenticated
// we store the path in the local storage to redirect to it after login
if (window.location.pathname !== '/') {
localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.pathname);
}
window.location.replace(`${baseApiUrl()}authenticate/`);
},
logout: () => {
window.location.replace(new URL('logout/', baseApiUrl()).href);
window.location.replace(`${baseApiUrl()}logout/`);
},
}));
+11 -8
View File
@@ -1,14 +1,17 @@
export const baseApiUrl = (apiVersion: string = '1.0') => {
const origin =
process.env.NEXT_PUBLIC_API_ORIGIN ||
(typeof window !== 'undefined' ? window.location.origin : '');
export const mediaUrl = () =>
process.env.NEXT_PUBLIC_MEDIA_URL ||
(typeof window !== 'undefined' ? window.location.origin : '');
return `${origin}/api/v${apiVersion}/`;
};
export const backendUrl = () =>
process.env.NEXT_PUBLIC_API_ORIGIN ||
(typeof window !== 'undefined' ? window.location.origin : '');
export const signalingUrl = (docId: string) => {
export const baseApiUrl = (apiVersion: string = '1.0') =>
`${backendUrl()}/api/v${apiVersion}/`;
export const providerUrl = (docId: string) => {
const base =
process.env.NEXT_PUBLIC_SIGNALING_URL ||
process.env.NEXT_PUBLIC_Y_PROVIDER_URL ||
(typeof window !== 'undefined' ? `wss://${window.location.host}/ws` : '');
return `${base}/${docId}`;
@@ -1,17 +1,4 @@
:root {
/**
* Input
*/
--c--components--forms-input--border-radius--hover: var(
--c--components--forms-input--border-radius
);
--c--components--forms-input--border-radius--focus: var(
--c--components--forms-input--border-radius
);
--c--components--forms-input--border-color--hover: var(
--c--components--forms-input--border-color
);
/**
* Datepicker
**/
@@ -1,338 +1,10 @@
@import url('@openfun/cunningham-react/icons');
@import url('@openfun/cunningham-react/style');
@import url('@openfun/cunningham-react/fonts');
@import url('./cunningham-tokens.css');
@import url('./cunningham-custom-tokens.css');
@import url('../assets/fonts/Marianne/Marianne-font.css');
.c__input,
.c__field,
.c__select,
.c__datagrid {
font-family: var(--c--theme--font--families--base);
}
.c__field {
line-height: initial;
}
.labelled-box label {
color: var(--c--theme--colors--primary-text);
}
.labelled-box--disabled label {
color: var(--c--components--forms-labelledbox--label-color--small-disabled);
}
.c__field :not(.c__textarea__wrapper, div) .labelled-box label.placeholder {
top: 50%;
transform: translateY(-50%);
}
/**
* Input
* TextArea
*/
.c__input__wrapper,
.c__textarea__wrapper {
transition: all var(--c--theme--transitions--duration)
var(--c--theme--transitions--ease-out);
}
.c__input__wrapper:has(input[readonly]),
.c__input__wrapper:has(input[readonly]) * {
cursor: default;
}
.c__textarea__wrapper:has(input.border-none),
.c__textarea__wrapper:has(input.border-none) *,
.c__input__wrapper:has(input.border-none),
.c__input__wrapper:has(input.border-none) * {
border: none;
}
.c__input__wrapper:hover,
.c__textarea__wrapper:hover {
box-shadow: var(--c--components--forms-input--box-shadow-color) 0 0 0 2px;
}
.c__textarea__wrapper--disabled:hover,
.c__input__wrapper--disabled:hover,
.c__input__wrapper:hover:has(input[readonly]) {
box-shadow: var(--c--theme--colors--primary-500) 0 0 0 0;
}
.c__input__wrapper--disabled {
color: var(--c--components--forms-input--value-color--disabled);
}
.c__input__wrapper .labelled-box__label.placeholder {
cursor: inherit;
}
.c__input__wrapper .c__input,
.c__textarea__wrapper .c__textarea {
width: 100%;
}
.c__input__wrapper--disabled .c__input {
color: var(--c--components--forms-input--value-color--disabled);
}
.c__input__wrapper--error .c__input {
color: var(--c--components--forms-input--color--error);
}
.c__input__wrapper--error:not(.c__input__wrapper--disabled):hover {
border-color: var(--c--components--forms-input--border--color-error-hover);
color: var(--c--components--forms-input--color--error-hover);
}
.c__input__wrapper--error:hover {
box-shadow: var(--c--components--forms-input--color--box-shadow-error-hover) 0
0 0 2px;
}
.c__input__wrapper--error:not(.c__input__wrapper--disabled):hover label {
color: var(--c--components--forms-input--border--color-error-hover);
}
input:-webkit-autofill,
input:-webkit-autofill:focus {
transition:
background-color 0s 600000s,
color 0s 600000s;
}
.c__textarea__wrapper .c__textarea {
color: var(--c--components--forms-textarea--color);
}
.c__textarea__wrapper:hover {
border-color: var(--c--components--forms-textarea--border-color-hover);
}
.c__textarea__wrapper--disabled:hover {
border-color: var(
--c--components--forms-textarea--disabled--border-color-hover
);
}
/**
* Select
*/
.c_select__no_border .c__select .c__select__wrapper,
.c_select__no_border .c__select .c__select__wrapper:hover,
.c_select__no_border
.c__select:not(.c__select--disabled)
.c__select__wrapper:hover {
border: none;
box-shadow: none;
}
.c__select__wrapper {
transition: all var(--c--theme--transitions--duration)
var(--c--theme--transitions--ease-out);
min-height: var(--c--components--forms-select--height);
height: auto;
}
.c__select:not(.c__select--disabled) .c__select__wrapper:hover {
box-shadow: var(--c--components--forms-input--box-shadow-color) 0 0 0 2px;
}
.c__select__wrapper:hover {
border-radius: var(--c--components--forms-select--border-radius-hover);
border-color: var(--c--components--forms-select--border-color-hover);
}
.c__select--disabled .c__select__wrapper:hover {
border-color: var(--c--components--forms-select--border-color-disabled-hover);
}
.c__select__menu__item {
transition: all var(--c--theme--transitions--duration)
var(--c--theme--transitions--ease-out);
}
.c__select--disabled .c__select__wrapper label,
.c__select--disabled .c__select__wrapper input,
.c_select__no_bg .c__select__wrapper {
background: none;
}
.c__select__wrapper:focus-within .labelled-box--disabled label {
color: var(--c--components--forms-labelledbox--label-color--small-disabled);
}
.c__select__wrapper .labelled-box {
display: flex;
gap: 0.6rem;
flex-direction: column;
align-items: flex-start;
}
.c__select__wrapper .labelled-box .labelled-box__children {
padding: unset;
padding-right: 5rem;
}
.c__select__wrapper .labelled-box .c__select__inner__actions {
right: 0;
top: 50%;
position: absolute;
}
.c__select__wrapper label {
position: relative;
padding-right: 5rem;
max-width: none;
}
.c__select__wrapper .c__select__inner__actions__open:focus {
outline: none;
}
.c__select__wrapper .labelled-box__label.c__offscreen {
display: none;
}
/**
* DataGrid
*/
.c__datagrid__table__container {
overflow: auto;
}
.c__datagrid__table__container > table th .c__datagrid__header {
color: var(--c--components--datagrid--header--color);
font-weight: var(--c--components--datagrid--header--weight);
font-size: var(--c--components--datagrid--header--size);
padding-block: 2rem;
}
.c__datagrid__table__container > table tbody tr {
border: none;
border-top: 1px var(--c--theme--colors--greyscale-100) solid;
border-bottom: 1px var(--c--theme--colors--greyscale-100) solid;
}
.c__datagrid__table__container > table tbody {
background-color: var(--c--components--datagrid--body--background-color);
}
.c__datagrid__table__container > table tbody tr:hover {
background-color: var(
--c--components--datagrid--body--background-color-hover
);
}
.c__datagrid__table__container > table {
table-layout: auto;
}
.c__datagrid__table__container > table td {
white-space: break-spaces;
}
.c__datagrid__table__container > table th:first-child,
.c__datagrid__table__container > table td:first-child {
padding-left: 2rem;
}
.c__datagrid > .c__pagination {
padding-inline: 1rem;
justify-content: flex-end;
}
.c__pagination__list {
gap: 3px;
border-radius: 4px;
background: var(--c--components--datagrid--pagination--background-color);
}
.c__pagination__list .c__button--tertiary-text.c__button--active {
background-color: var(
--c--components--datagrid--pagination--background-color-active
);
color: var(--c--theme--colors--greyscale-800);
}
.c__pagination__list .c__button--tertiary-text:disabled {
display: none;
}
@media (width <= 380px) {
.c__datagrid > .c__pagination {
flex-direction: column;
align-items: center;
gap: 1rem;
}
}
/**
* Date picker
*/
.c__popover.c__popover--borderless {
z-index: 3;
}
.c__date-picker__wrapper {
transition: all var(--c--theme--transitions--duration)
var(--c--theme--transitions--ease-out);
}
.c__date-picker:not(.c__date-picker--disabled):hover .c__date-picker__wrapper {
box-shadow: var(--c--theme--colors--primary-500) 0 0 0 2px;
}
.c__date-picker.c__date-picker--invalid:not(.c__date-picker--disabled):hover
.c__date-picker__wrapper {
box-shadow: var(--c--theme--colors--danger-300) 0 0 0 2px;
}
.c__date-picker__wrapper button[aria-label='Clear date'],
.c__date-picker.c__date-picker--invalid .c__date-picker__wrapper * {
color: var(--c--theme--colors--danger-300);
}
/**
* Others
*/
.c__checkbox:focus-within {
border-color: transparent;
background-color: transparent;
}
.c__checkbox {
transition: all 0.8s ease-in-out;
}
/**
* Button
*/
.c__button {
text-decoration: none;
}
.c__button:hover.c__button-no-bg,
.c__button.c__button-no-bg,
.c__button:disabled.c__button-no-bg {
background-color: transparent;
}
.c__button--medium {
padding: 0.9rem var(--c--theme--spacings--s);
}
.c__button--small {
padding: 0.6rem 0.75rem;
}
.c__button--with-icon--right {
padding: 0.7rem var(--c--theme--spacings--t) 0.7rem
var(--c--theme--spacings--s);
}
@import './cunningham-tokens';
@import './cunningham-custom-tokens';
@import '../assets/fonts/Marianne/Marianne-font';
@import '../assets/fonts/OpenSans/OpenSans-font.css';
.c__button--primary {
background-color: var(--c--components--button--primary--background--color);
@@ -355,117 +27,28 @@ input:-webkit-autofill:focus {
border-color: var(--c--components--button--primary--border--color-active);
}
.c__button--primary-text:active,
.c__button--primary-text.c__button--active {
border: none;
background-color: var(
--c--components--button--primary-text--background--color-active
);
/**
* DataGrid
*/
.c__datagrid__table__container > table th .c__datagrid__header {
color: var(--c--components--datagrid--header--color);
font-weight: var(--c--components--datagrid--header--weight);
font-size: var(--c--components--datagrid--header--size);
}
.c__button--primary-text:hover,
.c__button--primary-text:focus-visible {
background-color: var(
--c--components--button--primary-text--background--color-hover
);
color: var(--c--components--button--primary-text--color-hover);
.c__datagrid__table__container > table {
th:first-child,
td:first-child {
padding-left: 3rem;
}
}
.c__button:disabled {
background-color: var(--c--components--button--disabled--background--color);
color: var(--c--components--button--disabled--color);
}
.c__button--success {
background-color: var(--c--components--button--success--background--color);
color: var(--c--components--button--success--color);
}
.c__button--success:hover,
.c__button--success:focus-visible {
background-color: var(
--c--components--button--success--background--color-hover
);
color: var(--c--components--button--success--color-hover);
}
.c__button--success:disabled {
background-color: var(
--c--components--button--success--background--color-disabled
);
color: var(--c--components--button--success--color-disabled);
}
.c__button--secondary {
background-color: var(--c--components--button--secondary--background--color);
color: var(--c--components--button--secondary--color);
border: 1px solid var(--c--components--button--secondary--border--color);
}
.c__button--secondary:hover,
.c__button--secondary:focus-visible {
background-color: var(
--c--components--button--secondary--background--color-hover
);
color: var(--c--components--button--secondary--color-hover);
border: 1px solid var(--c--components--button--secondary--border--color-hover);
}
.c__button--tertiary {
color: var(--c--components--button--tertiary--color);
border: none;
}
.c__button--tertiary:hover,
.c__button--tertiary:focus-visible {
background-color: var(
--c--components--button--tertiary--background--color-hover
);
color: var(--c--components--button--tertiary--color);
}
.c__button--tertiary:disabled {
background-color: var(
--c--components--button--tertiary--background--color-disabled
);
color: var(--c--components--button--tertiary--color-disabled);
}
.c__button--tertiary-text {
border: none;
}
.c__button--tertiary-text:hover,
.c__button--tertiary-text:focus-visible {
background-color: var(
--c--components--button--tertiary-text--background--color-hover
);
color: var(--c--components--button--tertiary-text--color-hover);
}
.c__button--tertiary-text:disabled {
background-color: var(
--c--components--button--tertiary-text--background--color-disabled
);
color: var(--c--components--button--tertiary-text--color-disabled);
}
.c__button--danger {
background-color: var(--c--components--button--danger--background--color);
}
.c__button--danger:hover,
.c__button--danger:focus-visible {
background-color: var(
--c--components--button--danger--background--color-hover
);
color: var(--c--components--button--danger--color-hover);
}
.c__button--danger:disabled {
background-color: var(
--c--components--button--danger--background--color-disabled
);
.c__datagrid__table__container > table {
th:last-child,
td:last-child {
padding-right: 3rem;
}
}
/**
@@ -484,13 +67,279 @@ input:-webkit-autofill:focus {
padding: 1.5rem 1rem;
}
.c__modal--full .c__modal__content {
overflow-y: auto;
}
/**
* Toast
*/
.c__toast__container {
z-index: 10000;
}
/**
* DataGrid
*/
.cunningham-theme--dsfr {
.c__input,
.c__field,
.c__select,
.c__datagrid {
font-family: var(--c--theme--font--families--base);
}
/**
* DataGrid
*/
.c__datagrid__table__container > table th .c__datagrid__header {
color: var(--c--components--datagrid--header--color);
font-weight: var(--c--components--datagrid--header--weight);
font-size: var(--c--components--datagrid--header--size);
}
.c__datagrid__table__container > table tbody tr {
border: none;
border-top: 1px var(--c--theme--colors--greyscale-300) solid;
border-bottom: 1px var(--c--theme--colors--greyscale-300) solid;
}
.c__datagrid__table__container > table tbody {
background-color: var(--c--components--datagrid--body--background-color);
}
.c__datagrid__table__container > table tbody tr:hover {
background-color: var(
--c--components--datagrid--body--background-color-hover
);
}
.c__datagrid > .c__pagination {
padding-inline: 1rem;
justify-content: flex-end;
}
.c__pagination__list {
gap: 3px;
border-radius: 4px;
background: var(--c--components--datagrid--pagination--background-color);
}
.c__pagination__list .c__button--tertiary-text.c__button--active {
background-color: var(
--c--components--datagrid--pagination--background-color-active
);
color: var(--c--theme--colors--greyscale-800);
}
.c__pagination__list .c__button--tertiary-text:disabled {
display: none;
}
@media (width <= 380px) {
.c__datagrid > .c__pagination {
flex-direction: column;
align-items: center;
gap: 1rem;
}
}
/**
* Date picker
*/
.c__popover.c__popover--borderless {
z-index: 3;
}
.c__date-picker__wrapper {
transition: all var(--c--theme--transitions--duration)
var(--c--theme--transitions--ease-out);
}
.c__date-picker:not(.c__date-picker--disabled):hover
.c__date-picker__wrapper {
box-shadow: var(--c--theme--colors--primary-500) 0 0 0 2px;
}
.c__date-picker.c__date-picker--invalid:not(.c__date-picker--disabled):hover
.c__date-picker__wrapper {
box-shadow: var(--c--theme--colors--danger-300) 0 0 0 2px;
}
.c__date-picker__wrapper button[aria-label='Clear date'],
.c__date-picker.c__date-picker--invalid .c__date-picker__wrapper * {
color: var(--c--theme--colors--danger-300);
}
/**
* Others
*/
.c__checkbox:focus-within {
border-color: transparent;
background-color: transparent;
}
.c__checkbox {
transition: all 0.8s ease-in-out;
}
/**
* Button
*/
.c__button {
text-decoration: none;
}
.c__button:hover.c__button-no-bg,
.c__button.c__button-no-bg,
.c__button:disabled.c__button-no-bg {
background-color: transparent;
}
.c__button--medium {
padding: 0.9rem var(--c--theme--spacings--s);
}
.c__button--small {
padding: 0.6rem 0.75rem;
}
.c__button--with-icon--right {
padding: 0.7rem var(--c--theme--spacings--t) 0.7rem
var(--c--theme--spacings--s);
}
.c__button--primary-text:active,
.c__button--primary-text.c__button--active {
border: none;
background-color: var(
--c--components--button--primary-text--background--color-active
);
}
.c__button--tertiary:disabled {
background-color: var(--c--theme--colors--greyscale-100);
}
.c__button--success {
background-color: var(--c--components--button--success--background--color);
color: var(--c--components--button--success--color);
}
.c__button--success:hover,
.c__button--success:focus-visible {
background-color: var(
--c--components--button--success--background--color-hover
);
color: var(--c--components--button--success--color-hover);
}
.c__button--success:disabled {
background-color: var(
--c--components--button--success--background--color-disabled
);
color: var(--c--components--button--success--color-disabled);
}
.c__button--secondary {
background-color: var(
--c--components--button--secondary--background--color
);
color: var(--c--components--button--secondary--color);
border: 1px solid var(--c--components--button--secondary--border--color);
&:disabled {
background-color: var(--c--theme--colors--greyscale-000);
border-color: var(--c--theme--colors--greyscale-200);
}
}
.c__button--secondary:hover,
.c__button--secondary:focus-visible {
background-color: var(
--c--components--button--secondary--background--color-hover
);
color: var(--c--components--button--secondary--color-hover);
border: 1px solid
var(--c--components--button--secondary--border--color-hover);
}
.c__button--tertiary-text {
border: none;
}
.c__button--danger {
background-color: var(--c--components--button--danger--background--color);
}
.c__button--danger:hover,
.c__button--danger:focus-visible {
background-color: var(
--c--components--button--danger--background--color-hover
);
color: var(--c--components--button--danger--color-hover);
}
.c__button--danger:disabled {
background-color: var(
--c--components--button--danger--background--color-disabled
);
}
/**
* React Select
*/
.async-search-control {
border-radius: var(--c--components--forms-select--border-radius);
border-width: var(--c--components--forms-select--border-width);
border-color: var(--c--components--forms-select--border-color);
border-style: var(--c--components--forms-select--border-style);
background-color: var(--c--components--forms-select--background-color);
height: var(--c--components--forms-select--height);
padding: 0 12px;
}
.async-search-control:hover {
border-color: var(--c--components--forms-select--border-color);
}
.async-search-placeholder {
font-size: var(--c--components--forms-input--font-size);
font-weight: var(--c--components--forms-input--font-weight);
}
.async-search-separator {
display: none;
}
.async-search-value-container {
padding: 0;
}
/**
* Alert
*/
.c__alert--success {
border-color: var(--c--theme--colors--success-600);
}
.c__alert--info {
border-color: var(--c--theme--colors--info-600);
}
.c__alert--error {
border-color: var(--c--theme--colors--danger-600);
}
}
.cunningham-theme--openDesk {
.c__datagrid__table__container > table tbody tr {
border: 1px var(--c--theme--colors--greyscale-100) solid;
}
.c__button--with-icon--left {
padding: 0 1.5rem 0 var(--c--theme--spacings--s);
}
.c__button--medium {
padding: 0 var(--c--theme--spacings--s);
}
}
File diff suppressed because it is too large Load Diff
@@ -33,7 +33,7 @@ export const tokens = {
'greyscale-800': '#303C4B',
'greyscale-900': '#0C1A2B',
'greyscale-000': '#FFFFFF',
'primary-100': '#EDF5FA',
'primary-100': '#EBF2FC',
'primary-200': '#8CB5EA',
'primary-300': '#5894E1',
'primary-400': '#377FDB',
@@ -73,10 +73,6 @@ export const tokens = {
'success-text': '#FFFFFF',
'warning-text': '#FFFFFF',
'danger-text': '#FFFFFF',
'card-border': '#ededed',
'primary-bg': '#FAFAFA',
'primary-150': '#E5EEFA',
'info-150': '#E5EEFA',
},
font: {
sizes: {
@@ -94,13 +90,13 @@ export const tokens = {
t: '0.6875rem',
},
weights: {
thin: 100,
thin: 200,
light: 300,
regular: 400,
medium: 500,
bold: 600,
extrabold: 800,
black: 900,
extrabold: 700,
black: 800,
},
families: {
base: '"Roboto Flex Variable", sans-serif',
@@ -127,9 +123,6 @@ export const tokens = {
t: '0.5rem',
st: '0.25rem',
none: '0',
auto: 'auto',
bx: '2.2rem',
full: '100%',
},
transitions: {
'ease-in': 'cubic-bezier(0.32, 0, 0.67, 0)',
@@ -138,144 +131,36 @@ export const tokens = {
duration: '250ms',
},
breakpoints: {
xs: '480px',
xs: 0,
sm: '576px',
md: '768px',
lg: '992px',
xl: '1200px',
xxl: '1400px',
xxs: '320px',
},
logo: { src: '', widthHeader: '', widthFooter: '', alt: '' },
},
components: {
datagrid: {
header: {
weight: 'var(--c--theme--font--weights--extrabold)',
size: 'var(--c--theme--font--sizes--ml)',
},
cell: {
color: 'var(--c--theme--colors--primary-500)',
size: 'var(--c--theme--font--sizes--ml)',
},
card: {
'box-shadow': 'none',
'title-color': 'var(--c--theme--colors--primary-600)',
},
'forms-checkbox': {
'background-color': { hover: '#055fd214' },
color: 'var(--c--theme--colors--primary-500)',
'font-size': 'var(--c--theme--font--sizes--ml)',
pill: {
background: 'var(--c--theme--colors--primary-600)',
color: 'var(--c--theme--colors--greyscale-000)',
weight: 'bold',
radius: '3px',
'padding-x': '4px',
'padding-y': '0',
},
'forms-datepicker': {
'border-color': 'var(--c--theme--colors--primary-500)',
'value-color': 'var(--c--theme--colors--primary-500)',
'border-radius': {
hover: 'var(--c--components--forms-datepicker--border-radius)',
focus: 'var(--c--components--forms-datepicker--border-radius)',
},
},
'forms-field': {
color: 'var(--c--theme--colors--primary-500)',
'value-color': 'var(--c--theme--colors--primary-500)',
width: 'auto',
},
'forms-input': {
'value-color': 'var(--c--theme--colors--primary-500)',
'border-color': 'var(--c--theme--colors--primary-500)',
color: {
error: 'var(--c--theme--colors--danger-500)',
'error-hover': 'var(--c--theme--colors--danger-500)',
'box-shadow-error-hover': 'var(--c--theme--colors--danger-500)',
},
},
'forms-labelledbox': {
'label-color': {
small: 'var(--c--theme--colors--primary-500)',
'small-disabled': 'var(--c--theme--colors--greyscale-400)',
big: { disabled: 'var(--c--theme--colors--greyscale-400)' },
},
},
'forms-select': {
'border-color': 'var(--c--theme--colors--primary-500)',
'border-color-disabled-hover':
'var(--c--theme--colors--greyscale-200)',
'border-radius': {
hover: 'var(--c--components--forms-select--border-radius)',
focus: 'var(--c--components--forms-select--border-radius)',
},
'font-size': 'var(--c--theme--font--sizes--ml)',
'menu-background-color': '#ffffff',
'item-background-color': {
hover: 'var(--c--theme--colors--primary-300)',
},
},
'forms-switch': {
'accent-color': 'var(--c--theme--colors--primary-400)',
},
'forms-textarea': {
'border-color': 'var(--c--components--forms-textarea--border-color)',
'border-color-hover':
'var(--c--components--forms-textarea--border-color)',
'border-radius': {
hover: 'var(--c--components--forms-textarea--border-radius)',
focus: 'var(--c--components--forms-textarea--border-radius)',
},
color: 'var(--c--theme--colors--primary-500)',
disabled: {
'border-color-hover': 'var(--c--theme--colors--greyscale-200)',
},
},
modal: { 'background-color': '#ffffff' },
button: {
'border-radius': {
active: 'var(--c--components--button--border-radius)',
},
'medium-height': 'auto',
'small-height': 'auto',
success: {
color: 'white',
'color-disabled': 'white',
'color-hover': 'white',
background: {
color: 'var(--c--theme--colors--success-600)',
'color-disabled': 'var(--c--theme--colors--greyscale-300)',
'color-hover': 'var(--c--theme--colors--success-800)',
},
},
danger: {
'color-hover': 'white',
background: {
color: 'var(--c--theme--colors--danger-400)',
'color-hover': 'var(--c--theme--colors--danger-500)',
'color-disabled': 'var(--c--theme--colors--danger-100)',
},
},
primary: {
color: 'var(--c--theme--colors--primary-text)',
'color-active': 'var(--c--theme--colors--primary-text)',
background: {
color: 'var(--c--theme--colors--primary-400)',
'color-active': 'var(--c--theme--colors--primary-500)',
},
border: { 'color-active': 'transparent' },
},
secondary: {
color: 'var(--c--theme--colors--primary-500)',
'color-hover': 'var(--c--theme--colors--primary-text)',
background: {
color: 'white',
'color-hover': 'var(--c--theme--colors--primary-700)',
},
border: { color: 'var(--c--theme--colors--primary-200)' },
},
tertiary: {
color: 'var(--c--theme--colors--primary-text)',
'color-disabled': 'var(--c--theme--colors--greyscale-600)',
background: {
'color-hover': 'var(--c--theme--colors--primary-100)',
'color-disabled': 'var(--c--theme--colors--greyscale-200)',
},
},
disabled: { color: 'white', background: { color: '#b3cef0' } },
strip: { color: 'var(--c--theme--colors--danger-500)' },
grid: { color: 'var(--c--theme--colors--danger-900)' },
header: {
background: 'var(--c--theme--colors--greyscale-000)',
'title-color': 'var(--c--theme--colors--primary-800)',
},
footer: { background: 'var(--c--theme--colors--greyscale-000)' },
main: { background: 'var(--c--theme--colors--greyscale-100)' },
languagePicker: { image: '/assets/icon-language.svg' },
},
},
dark: {
@@ -333,8 +218,6 @@ export const tokens = {
dsfr: {
theme: {
colors: {
'card-border': '#ededed',
'primary-text': '#000091',
'primary-100': '#f5f5fe',
'primary-150': '#F4F4FD',
'primary-200': '#ececfe',
@@ -345,7 +228,6 @@ export const tokens = {
'primary-700': '#272747',
'primary-800': '#21213f',
'primary-900': '#1c1a36',
'secondary-text': '#FFFFFF',
'secondary-100': '#fee9ea',
'secondary-200': '#fedfdf',
'secondary-300': '#fdbfbf',
@@ -355,8 +237,7 @@ export const tokens = {
'secondary-700': '#3b2424',
'secondary-800': '#341f1f',
'secondary-900': '#2b1919',
'greyscale-text': '#303C4B',
'greyscale-000': '#f6f6f6',
'greyscale-000': '#ffffff',
'greyscale-100': '#eeeeee',
'greyscale-200': '#e5e5e5',
'greyscale-300': '#e1e1e1',
@@ -416,13 +297,16 @@ export const tokens = {
},
},
components: {
alert: { 'border-radius': '0' },
alert: {
'border-radius': '0',
'background-color': 'var(--c--theme--colors--greyscale-000)',
},
button: {
'medium-height': '48px',
'border-radius': '4px',
'border-radius': '0',
primary: {
background: {
color: 'var(--c--theme--colors--primary-text)',
color: 'var(--c--theme--colors--primary-600)',
'color-hover': '#1212ff',
'color-active': '#2323ff',
},
@@ -443,7 +327,7 @@ export const tokens = {
color: 'var(--c--theme--colors--primary-600)',
'color-hover': 'var(--c--theme--colors--primary-600)',
},
color: 'var(--c--theme--colors--primary-text)',
color: 'var(--c--theme--colors--primary-600)',
},
'tertiary-text': {
background: {
@@ -452,9 +336,13 @@ export const tokens = {
'color-hover': 'var(--c--theme--colors--primary-text)',
},
},
card: {
'box-shadow': '2px 2px 5px var(--c--theme--colors--greyscale-300)',
'title-color': 'var(--c--theme--colors--primary-600)',
},
datagrid: {
header: {
color: 'var(--c--theme--colors--primary-text)',
color: 'var(--c--theme--colors--primary-600)',
size: 'var(--c--theme--font--sizes--s)',
},
body: {
@@ -466,39 +354,327 @@ export const tokens = {
'background-color-active': 'var(--c--theme--colors--primary-300)',
},
},
'forms-checkbox': {
'forms-datepicker': { 'border-radius': '0' },
'forms-fileuploader': { 'border-radius': '0' },
'forms-input': {
'background-color': 'var(--c--theme--colors--greyscale-100)',
'border-radius': '0',
color: 'var(--c--theme--colors--primary-text)',
'border-color': 'var(--c--theme--colors--greyscale-900)',
'border-width': '0 0 2px 0',
'border-color--focus': '#0974F6',
'border-color--hover': 'var(--c--theme--colors--greyscale-900)',
'label-color--focus':
'var(--c--components--forms-labelledbox--label-color--small)',
},
'forms-textarea': {
'background-color': 'var(--c--theme--colors--greyscale-100)',
'border-radius': '0',
'border-color': 'var(--c--theme--colors--greyscale-900)',
'border-width': '0 0 2px 0',
'border-color--focus': '#0974F6',
'border-color--hover': 'var(--c--theme--colors--greyscale-900)',
'label-color--focus':
'var(--c--components--forms-labelledbox--label-color--small)',
},
'forms-select': {
'background-color': 'var(--c--theme--colors--greyscale-100)',
'border-radius': '0',
'border-color': 'var(--c--theme--colors--greyscale-900)',
'border-width': '0 0 2px 0',
'border-color--focus': '#0974F6',
'border-color--hover': 'var(--c--theme--colors--greyscale-900)',
'label-color--focus':
'var(--c--components--forms-labelledbox--label-color--big)',
},
'forms-switch': { 'accent-color': '#2323ff' },
'forms-checkbox': { 'accent-color': '#2323ff' },
},
},
dsfrDark: {
theme: {
colors: {
'primary-100': '#1a1a2e',
'primary-150': '#21213f',
'primary-200': '#272747',
'primary-300': '#3b3b61',
'primary-400': '#535380',
'primary-500': '#6a6af4',
'primary-600': '#8b8bf9',
'primary-700': '#a1a1f5',
'primary-800': '#c1c1ff',
'primary-900': '#ececfe',
'secondary-100': '#2b1919',
'secondary-200': '#341f1f',
'secondary-300': '#3b2424',
'secondary-400': '#5e2b2b',
'secondary-500': '#c91a1f',
'secondary-600': '#e6454a',
'secondary-700': '#f06062',
'secondary-800': '#fca7a9',
'secondary-900': '#fee9ea',
'greyscale-000': '#161616',
'greyscale-100': '#1e1e1e',
'greyscale-200': '#242424',
'greyscale-300': '#2a2a2a',
'greyscale-400': '#2f2f2f',
'greyscale-500': '#353535',
'greyscale-600': '#3a3a3a',
'greyscale-700': '#929292',
'greyscale-800': '#7b7b7b',
'greyscale-900': '#eeeeee',
'success-text': '#88fdaa',
'success-100': '#1e2e22',
'success-200': '#204129',
'success-300': '#18753c',
'success-400': '#1f8d49',
'success-500': '#3bea7e',
'success-600': '#66f2a1',
'success-700': '#88fdaa',
'success-800': '#b8fec9',
'success-900': '#dffee6',
'info-text': '#0078f3',
'info-100': '#1d2437',
'info-200': '#222a3f',
'info-300': '#293145',
'info-400': '#3b4c6b',
'info-500': '#0078f3',
'info-600': '#3391ff',
'info-700': '#66aaff',
'info-800': '#99ccff',
'info-900': '#cce5ff',
'warning-text': '#ffbeb4',
'warning-100': '#361e19',
'warning-200': '#3e241e',
'warning-300': '#5e2c21',
'warning-400': '#b34000',
'warning-500': '#d64d00',
'warning-600': '#ff6a34',
'warning-700': '#ff8b66',
'warning-800': '#ffb299',
'warning-900': '#ffe9e6',
'danger-text': '#fddede',
'danger-100': '#3a1c1c',
'danger-200': '#412121',
'danger-300': '#642727',
'danger-400': '#c9191e',
'danger-500': '#e1000f',
'danger-600': '#f5434a',
'danger-700': '#f76f71',
'danger-800': '#fdbfbf',
'danger-900': '#fee9e9',
},
font: { families: { accent: 'Marianne', base: 'Marianne' } },
logo: {
src: '/assets/logo-gouv-dark.svg',
widthHeader: '110px',
widthFooter: '220px',
alt: 'Gouvernement Logo',
},
},
components: {
alert: {
'border-radius': '0',
'background-color': 'var(--c--theme--colors--greyscale-000)',
},
button: {
'medium-height': '48px',
'border-radius': '0',
primary: {
background: {
color: 'var(--c--theme--colors--primary-600)',
'color-hover': '#1212ff',
'color-active': '#2323ff',
},
color: 'var(--c--theme--colors--greyscale-000)',
'color-hover': '#ffffff',
'color-active': '#ffffff',
},
'primary-text': {
background: {
'color-hover': 'var(--c--theme--colors--primary-100)',
'color-active': 'var(--c--theme--colors--primary-100)',
},
'color-hover': 'var(--c--theme--colors--primary-text)',
},
secondary: {
background: { 'color-hover': '#F6F6F6', 'color-active': '#EDEDED' },
border: {
color: 'var(--c--theme--colors--primary-600)',
'color-hover': 'var(--c--theme--colors--primary-600)',
},
color: 'var(--c--theme--colors--primary-600)',
},
'tertiary-text': {
background: {
'color-hover': 'var(--c--theme--colors--primary-100)',
},
'color-hover': 'var(--c--theme--colors--primary-text)',
},
},
card: {
'box-shadow': '2px 2px 5px var(--c--theme--colors--greyscale-300)',
'title-color': 'var(--c--theme--colors--primary-900)',
},
datagrid: {
header: {
color: 'var(--c--theme--colors--primary-600)',
size: 'var(--c--theme--font--sizes--s)',
},
body: {
'background-color': 'transparent',
'background-color-hover': '#F4F4FD',
},
pagination: {
'background-color': 'transparent',
'background-color-active': 'var(--c--theme--colors--primary-300)',
},
},
'forms-datepicker': { 'border-radius': '0' },
'forms-fileuploader': { 'border-radius': '0' },
'forms-field': { color: 'var(--c--theme--colors--primary-text)' },
'forms-input': {
'border-radius': '4px',
'background-color': '#ffffff',
'border-color': 'var(--c--theme--colors--primary-text)',
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
'value-color': 'var(--c--theme--colors--primary-text)',
'font-size': '14px',
'background-color': 'var(--c--theme--colors--greyscale-100)',
'border-radius': '0',
'border-color': 'var(--c--theme--colors--greyscale-900)',
'border-width': '0 0 2px 0',
'border-color--focus': '#0974F6',
'border-color--hover': 'var(--c--theme--colors--greyscale-900)',
'label-color--focus':
'var(--c--components--forms-labelledbox--label-color--small)',
},
'forms-labelledbox': {
'label-color': { big: 'var(--c--theme--colors--primary-text)' },
'forms-textarea': {
'background-color': 'var(--c--theme--colors--greyscale-100)',
'border-radius': '0',
'border-color': 'var(--c--theme--colors--greyscale-900)',
'border-width': '0 0 2px 0',
'border-color--focus': '#0974F6',
'border-color--hover': 'var(--c--theme--colors--greyscale-900)',
'label-color--focus':
'var(--c--components--forms-labelledbox--label-color--small)',
},
'forms-select': {
'item-font-size': '14px',
'border-radius': '4px',
'border-radius-hover': '4px',
'background-color': '#ffffff',
'border-color': 'var(--c--theme--colors--primary-text)',
'border-color-hover': 'var(--c--theme--colors--primary-text)',
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
'background-color': 'var(--c--theme--colors--greyscale-100)',
'border-radius': '0',
'border-color': 'var(--c--theme--colors--greyscale-900)',
'border-width': '0 0 2px 0',
'border-color--focus': '#0974F6',
'border-color--hover': 'var(--c--theme--colors--greyscale-900)',
'label-color--focus':
'var(--c--components--forms-labelledbox--label-color--big)',
},
'forms-switch': {
'handle-border-radius': '2px',
'rail-border-radius': '4px',
'accent-color': 'var(--c--theme--colors--primary-text)',
'forms-switch': { 'accent-color': '#2323ff' },
'forms-checkbox': { 'accent-color': '#2323ff' },
header: {
background: 'var(--c--theme--colors--greyscale-100)',
'title-color': 'var(--c--theme--colors--greyscale-900)',
},
'forms-textarea': { 'border-radius': '0' },
footer: { background: 'var(--c--theme--colors--greyscale-100)' },
main: { background: 'var(--c--theme--colors--greyscale-000)' },
languagePicker: { image: '/assets/icon-language-dark.svg' },
},
},
openDesk: {
theme: {
colors: {
'primary-100': '#F7F5FF',
'primary-200': '#ECE7FE',
'primary-300': '#DCD2FE',
'primary-400': '#C8B9FD',
'primary-500': '#8E75FA',
'primary-600': '#7051FA',
'primary-700': '#571EFA',
'primary-800': '#4519C2',
'primary-900': '#341291',
'secondary-100': '#EDFDFB',
'secondary-200': '#BFF9F2',
'secondary-300': '#71EFE1',
'secondary-400': '#00E6CC',
'secondary-500': '#00A896',
'secondary-600': '#008A7B',
'secondary-700': '#006C60',
'secondary-800': '#00564D',
'secondary-900': '#004039',
'greyscale-000': '#ffffff',
'greyscale-100': '#EEEFF2',
'greyscale-200': '#D3D7DE',
'greyscale-300': '#B6BCC8',
'greyscale-400': '#7C879C',
'greyscale-500': '#637089',
'greyscale-600': '#4D5B79',
'greyscale-700': '#364768',
'greyscale-800': '#203257',
'greyscale-900': '#1e1e1e',
},
font: { families: { accent: 'Open Sans', base: 'Open Sans' } },
logo: {
src: '/assets/logo-opendesk.svg',
widthHeader: '110px',
widthFooter: '220px',
alt: 'Gouvernement Logo',
},
},
components: {
alert: {
'border-radius': '0',
'background-color': 'var(--c--theme--colors--greyscale-000)',
},
button: {
'medium-height': '48px',
'border-radius': '8px',
'border-radius--active': '8px',
'font-weight': '600',
primary: {
background: {
color: 'var(--c--theme--colors--primary-700)',
'color-hover': 'var(--c--theme--colors--primary-900)',
'color-active': 'var(--c--theme--colors--primary-900)',
},
color: '#ffffff',
'color-hover': '#ffffff',
'color-active': '#ffffff',
},
'primary-text': {
background: {
'color-hover': 'var(--c--theme--colors--primary-100)',
'color-active': 'var(--c--theme--colors--primary-100)',
},
'color-hover': 'var(--c--theme--colors--primary-text)',
},
secondary: {
background: { 'color-hover': '#F6F6F6', 'color-active': '#EDEDED' },
border: {
color: 'var(--c--theme--colors--primary-600)',
'color-hover': 'var(--c--theme--colors--primary-600)',
},
color: 'var(--c--theme--colors--primary-600)',
},
'tertiary-text': {
background: {
'color-hover': 'var(--c--theme--colors--primary-100)',
},
'color-hover': 'var(--c--theme--colors--primary-text)',
},
},
card: { 'title-color': 'var(--c--theme--colors--greyscale-900)' },
datagrid: {
header: {
color: 'var(--c--theme--colors--greyscale-900)',
size: 'var(--c--theme--font--sizes--s)',
},
body: {
'background-color': 'transparent',
'background-color-hover': '#F4F4FD',
},
},
pill: {
background: 'var(--c--theme--colors--primary-300)',
color: 'var(--c--theme--colors--greyscale-900)',
weight: '500',
radius: '8px',
'padding-x': '16px',
'padding-y': '2px',
},
strip: { color: 'var(--c--theme--colors--primary-300)' },
grid: { color: 'var(--c--theme--colors--greyscale-500)' },
},
},
},
+2 -1
View File
@@ -20,7 +20,8 @@ declare module '*.svg?url' {
namespace NodeJS {
interface ProcessEnv {
NEXT_PUBLIC_API_ORIGIN?: string;
NEXT_PUBLIC_SIGNALING_URL?: string;
NEXT_PUBLIC_MEDIA_URL?: string;
NEXT_PUBLIC_Y_PROVIDER_URL?: string;
NEXT_PUBLIC_SW_DEACTIVATED?: string;
NEXT_PUBLIC_THEME?: string;
}
@@ -0,0 +1,36 @@
import { useMutation } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { DocAttachment } from '../types';
interface CreateDocAttachment {
docId: string;
body: FormData;
}
export const createDocAttachment = async ({
docId,
body,
}: CreateDocAttachment): Promise<DocAttachment> => {
const response = await fetchAPI(`documents/${docId}/attachment-upload/`, {
method: 'POST',
body,
withoutContentType: true,
});
if (!response.ok) {
throw new APIError(
'Failed to upload on the doc',
await errorCauses(response),
);
}
return response.json() as Promise<DocAttachment>;
};
export function useCreateDocAttachment() {
return useMutation<DocAttachment, APIError, CreateDocAttachment>({
mutationFn: createDocAttachment,
});
}
@@ -2,20 +2,36 @@ import { BlockNoteEditor as BlockNoteEditorCore } from '@blocknote/core';
import '@blocknote/core/fonts/inter.css';
import { BlockNoteView } from '@blocknote/mantine';
import '@blocknote/mantine/style.css';
import React, { useEffect, useMemo } from 'react';
import { WebrtcProvider } from 'y-webrtc';
import { HocuspocusProvider } from '@hocuspocus/provider';
import React, { useCallback, useEffect, useMemo } from 'react';
import { Box } from '@/components';
import { Box, TextErrors } from '@/components';
import { mediaUrl } from '@/core';
import { useAuthStore } from '@/core/auth';
import { Doc } from '@/features/docs/doc-management';
import { Version } from '@/features/docs/doc-versioning/';
import { useCreateDocAttachment } from '../api/useCreateDocUpload';
import useSaveDoc from '../hook/useSaveDoc';
import { useDocStore } from '../stores';
import { randomColor } from '../utils';
import { BlockNoteToolbar } from './BlockNoteToolbar';
const cssEditor = `
&, & > .bn-container, & .ProseMirror {
height:100%
};
& .collaboration-cursor__caret.ProseMirror-widget{
word-wrap: initial;
}
& .bn-inline-content code {
background-color: gainsboro;
padding: 2px;
border-radius: 4px;
}
`;
interface BlockNoteEditorProps {
doc: Doc;
version?: Version;
@@ -28,7 +44,7 @@ export const BlockNoteEditor = ({ doc, version }: BlockNoteEditorProps) => {
const provider = docsStore?.[storeId]?.provider;
useEffect(() => {
if (!provider || provider.doc.guid !== storeId) {
if (!provider || provider.document.guid !== storeId) {
createProvider(storeId, initialContent);
}
}, [createProvider, initialContent, provider, storeId]);
@@ -42,7 +58,7 @@ export const BlockNoteEditor = ({ doc, version }: BlockNoteEditorProps) => {
interface BlockNoteContentProps {
doc: Doc;
provider: WebrtcProvider;
provider: HocuspocusProvider;
storeId: string;
}
@@ -55,9 +71,29 @@ export const BlockNoteContent = ({
const { userData } = useAuthStore();
const { setStore, docsStore } = useDocStore();
const canSave = doc.abilities.partial_update && !isVersion;
useSaveDoc(doc.id, provider.doc, canSave);
useSaveDoc(doc.id, provider.document, canSave);
const storedEditor = docsStore?.[storeId]?.editor;
const {
mutateAsync: createDocAttachment,
isError: isErrorAttachment,
error: errorAttachment,
} = useCreateDocAttachment();
const uploadFile = useCallback(
async (file: File) => {
const body = new FormData();
body.append('file', file);
const ret = await createDocAttachment({
docId: doc.id,
body,
});
return `${mediaUrl()}${ret.file}`;
},
[createDocAttachment, doc.id],
);
const editor = useMemo(() => {
if (storedEditor) {
return storedEditor;
@@ -66,30 +102,28 @@ export const BlockNoteContent = ({
return BlockNoteEditorCore.create({
collaboration: {
provider,
fragment: provider.doc.getXmlFragment('document-store'),
fragment: provider.document.getXmlFragment('document-store'),
user: {
name: userData?.email || 'Anonymous',
color: randomColor(),
},
},
uploadFile,
});
}, [provider, storedEditor, userData?.email]);
}, [provider, storedEditor, uploadFile, userData?.email]);
useEffect(() => {
setStore(storeId, { editor });
}, [setStore, storeId, editor]);
return (
<Box
$css={`
&, & > .bn-container, & .ProseMirror {
height:100%
};
& .collaboration-cursor__caret.ProseMirror-widget{
word-wrap: initial;
}
`}
>
<Box $css={cssEditor}>
{isErrorAttachment && (
<Box $margin={{ bottom: 'big' }}>
<TextErrors causes={errorAttachment.cause} />
</Box>
)}
<BlockNoteView
editor={editor}
formattingToolbar={false}
@@ -11,9 +11,10 @@ import {
UnnestBlockButton,
useBlockNoteEditor,
useComponentsContext,
useSelectedBlocks,
} from '@blocknote/react';
import { forEach, isArray } from 'lodash';
import React from 'react';
import React, { useMemo } from 'react';
export const BlockNoteToolbar = () => {
return (
@@ -91,6 +92,7 @@ const recursiveContent = (content: Block[], base: string = '') => {
export function MarkdownButton() {
const editor = useBlockNoteEditor();
const Components = useComponentsContext();
const selectedBlocks = useSelectedBlocks(editor);
const handleConvertMarkdown = () => {
const blocks = editor.getSelection()?.blocks;
@@ -114,7 +116,11 @@ export function MarkdownButton() {
});
};
if (!Components) {
const show = useMemo(() => {
return !!selectedBlocks.find((block) => block.content !== undefined);
}, [selectedBlocks]);
if (!show || !editor.isEditable || !Components) {
return null;
}
@@ -5,10 +5,17 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Card, Text, TextErrors } from '@/components';
import { Panel } from '@/components/Panel';
import { useCunninghamTheme } from '@/cunningham';
import { DocHeader } from '@/features/docs/doc-header';
import { Doc } from '@/features/docs/doc-management';
import { Versions, useDocVersion } from '@/features/docs/doc-versioning/';
import { Summary, useDocSummaryStore } from '@/features/docs/doc-summary';
import {
VersionList,
Versions,
useDocVersion,
useDocVersionStore,
} from '@/features/docs/doc-versioning/';
import { BlockNoteEditor } from './BlockNoteEditor';
@@ -20,6 +27,8 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
const {
query: { versionId },
} = useRouter();
const { isPanelVersionOpen, setIsPanelVersionOpen } = useDocVersionStore();
const { isPanelSummaryOpen, setIsPanelSummaryOpen } = useDocSummaryStore();
const { t } = useTranslation();
@@ -29,7 +38,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
return (
<>
<DocHeader doc={doc} />
<DocHeader doc={doc} versionId={versionId as Versions['version_id']} />
{!doc.abilities.partial_update && (
<Box $margin={{ all: 'small', top: 'none' }}>
<Alert type={VariantType.WARNING}>
@@ -58,6 +67,16 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
<BlockNoteEditor doc={doc} />
)}
</Card>
{doc.abilities.versions_list && isPanelVersionOpen && (
<Panel title={t('VERSIONS')} setIsPanelOpen={setIsPanelVersionOpen}>
<VersionList doc={doc} />
</Panel>
)}
{isPanelSummaryOpen && (
<Panel title={t('SUMMARY')} setIsPanelOpen={setIsPanelSummaryOpen}>
<Summary doc={doc} />
</Panel>
)}
</Box>
</>
);
@@ -1,32 +1,14 @@
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
import { useRouter } from 'next/router';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import * as Y from 'yjs';
import { useUpdateDoc } from '@/features/docs/doc-management/';
import { KEY_LIST_DOC_VERSIONS } from '@/features/docs/doc-versioning';
import { useDocStore } from '../stores';
import { toBase64 } from '../utils';
const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
const { toast } = useToastProvider();
const { t } = useTranslation();
const { forceSave, setForceSave } = useDocStore();
const { mutate: updateDoc } = useUpdateDoc({
onSuccess: (data) => {
toast(
t('Your document "{{docTitle}}" has been saved.', {
docTitle: data.title,
}),
VariantType.SUCCESS,
{
duration: 1500,
},
);
},
listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
});
const [initialDoc, setInitialDoc] = useState<string>(
@@ -83,18 +65,6 @@ const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
});
}, [doc, docId, updateDoc]);
useEffect(() => {
if (forceSave === 'false') {
return;
}
setForceSave('false');
if ((forceSave === 'current' && hasChanged()) || forceSave === 'version') {
saveDoc();
}
}, [forceSave, hasChanged, saveDoc, setForceSave]);
const timeout = useRef<NodeJS.Timeout>();
const router = useRouter();
@@ -1,2 +1,3 @@
export * from './components';
export * from './stores';
export * from './utils';
@@ -1,34 +1,26 @@
import { BlockNoteEditor } from '@blocknote/core';
import { WebrtcProvider } from 'y-webrtc';
import { HocuspocusProvider } from '@hocuspocus/provider';
import * as Y from 'yjs';
import { create } from 'zustand';
import { signalingUrl } from '@/core';
import { providerUrl } from '@/core';
import { Base64 } from '@/features/docs/doc-management';
interface DocStore {
provider: WebrtcProvider;
provider: HocuspocusProvider;
editor?: BlockNoteEditor;
}
type ForceSaveState = 'false' | 'version' | 'current';
export interface UseDocStore {
docsStore: {
[storeId: string]: DocStore;
};
createProvider: (storeId: string, initialDoc: Base64) => WebrtcProvider;
createProvider: (storeId: string, initialDoc: Base64) => HocuspocusProvider;
setStore: (storeId: string, props: Partial<DocStore>) => void;
forceSave: ForceSaveState;
setForceSave: (forceSave: ForceSaveState) => void;
}
export const useDocStore = create<UseDocStore>((set, get) => ({
docsStore: {},
forceSave: 'false',
setForceSave: (forceSave) => {
set(() => ({ forceSave }));
},
createProvider: (storeId: string, initialDoc: Base64) => {
const doc = new Y.Doc({
guid: storeId,
@@ -38,9 +30,10 @@ export const useDocStore = create<UseDocStore>((set, get) => ({
Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64'));
}
const provider = new WebrtcProvider(storeId, doc, {
signaling: [signalingUrl(storeId)],
maxConns: 5,
const provider = new HocuspocusProvider({
url: providerUrl(storeId),
name: storeId,
document: doc,
});
get().setStore(storeId, { provider });
@@ -0,0 +1,3 @@
export interface DocAttachment {
file: string;
}
@@ -1,4 +1,5 @@
import React, { Fragment } from 'react';
import { Button } from '@openfun/cunningham-react';
import React, { Fragment, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Card, StyledLink, Text } from '@/components';
@@ -9,96 +10,115 @@ import {
currentDocRole,
useTransRole,
} from '@/features/docs/doc-management';
import { ModalVersion, Versions } from '@/features/docs/doc-versioning';
import { useDate } from '@/hook';
import { DocTagPublic } from './DocTagPublic';
import { DocToolBox } from './DocToolBox';
interface DocHeaderProps {
doc: Doc;
versionId?: Versions['version_id'];
}
export const DocHeader = ({ doc }: DocHeaderProps) => {
export const DocHeader = ({ doc, versionId }: DocHeaderProps) => {
const { colorsTokens } = useCunninghamTheme();
const { t } = useTranslation();
const { formatDate } = useDate();
const transRole = useTransRole();
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
return (
<Card
$margin="small"
aria-label={t('It is the card information about the document.')}
>
<Box $padding="small" $direction="row" $align="center">
<StyledLink href="/">
<Text
$isMaterialIcon
$theme="primary"
$size="2rem"
$css={`&:hover {background-color: ${colorsTokens()['primary-100']}; };`}
$hasTransition
$radius="5px"
$padding="tiny"
>
home
</Text>
</StyledLink>
<Box
$width="1px"
$height="70%"
$background={colorsTokens()['greyscale-100']}
$margin={{ horizontal: 'small' }}
/>
<Text as="h2" $align="center" $margin={{ all: 'none', left: 'tiny' }}>
{doc.title}
</Text>
<DocToolBox doc={doc} />
</Box>
<Box
$direction="row"
$align="center"
$css="border-top:1px solid #eee"
$padding={{ horizontal: 'big', vertical: 'tiny' }}
$gap="0.5rem 2rem"
$justify="space-between"
$wrap="wrap"
<>
<Card
$margin="small"
aria-label={t('It is the card information about the document.')}
>
<Box $direction="row" $align="center" $gap="0.5rem 2rem" $wrap="wrap">
{doc.is_public && (
<Box $padding="small" $direction="row" $align="center">
<StyledLink href="/">
<Text
$weight="bold"
$background={colorsTokens()['primary-600']}
$color="white"
$padding="xtiny"
$radius="3px"
$size="s"
$isMaterialIcon
$theme="primary"
$variation="700"
$size="2rem"
$css={`&:hover {background-color: ${colorsTokens()['primary-100']}; };`}
$hasTransition
$radius="5px"
$padding="tiny"
>
{t('Public')}
home
</Text>
)}
</StyledLink>
<Box
$width="1px"
$height="70%"
$background={colorsTokens()['greyscale-100']}
$margin={{ horizontal: 'small' }}
/>
<Box $gap="1rem" $direction="row">
<Text
as="h2"
$align="center"
$margin={{ all: 'none', left: 'tiny' }}
>
{doc.title}
</Text>
{versionId && (
<Button
onClick={() => {
setIsModalVersionOpen(true);
}}
size="small"
>
{t('Restore this version')}
</Button>
)}
</Box>
<DocToolBox doc={doc} />
</Box>
<Box
$direction="row"
$align="center"
$css="border-top:1px solid #eee"
$padding={{ horizontal: 'big', vertical: 'tiny' }}
$gap="0.5rem 2rem"
$justify="space-between"
$wrap="wrap"
>
<Box $direction="row" $align="center" $gap="0.5rem 2rem" $wrap="wrap">
<DocTagPublic />
<Text $size="s" $display="inline">
{t('Created at')} <strong>{formatDate(doc.created_at)}</strong>
</Text>
<Text $size="s" $display="inline" $elipsis $maxWidth="60vw">
{t('Owners:')}{' '}
<strong>
{doc.accesses
.filter(
(access) => access.role === Role.OWNER && access.user.email,
)
.map((access, index, accesses) => (
<Fragment key={`access-${index}`}>
{access.user.email}{' '}
{index < accesses.length - 1 ? ' / ' : ''}
</Fragment>
))}
</strong>
</Text>
</Box>
<Text $size="s" $display="inline">
{t('Created at')} <strong>{formatDate(doc.created_at)}</strong>
</Text>
<Text $size="s" $display="inline" $elipsis $maxWidth="60vw">
{t('Owners:')}{' '}
<strong>
{doc.accesses
.filter(
(access) => access.role === Role.OWNER && access.user.email,
)
.map((access, index, accesses) => (
<Fragment key={`access-${index}`}>
{access.user.email}{' '}
{index < accesses.length - 1 ? ' / ' : ''}
</Fragment>
))}
</strong>
{t('Your role:')}{' '}
<strong>{transRole(currentDocRole(doc.abilities))}</strong>
</Text>
</Box>
<Text $size="s" $display="inline">
{t('Your role:')}{' '}
<strong>{transRole(currentDocRole(doc.abilities))}</strong>
</Text>
</Box>
</Card>
</Card>
{isModalVersionOpen && versionId && (
<ModalVersion
onClose={() => setIsModalVersionOpen(false)}
docId={doc.id}
versionId={versionId}
/>
)}
</>
);
};
@@ -0,0 +1,39 @@
import { useRouter } from 'next/router';
import { useTranslation } from 'react-i18next';
import { Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { KEY_DOC_VISIBILITY, useDoc } from '@/features/docs/doc-management';
export const DocTagPublic = () => {
const { colorsTokens } = useCunninghamTheme();
const { t } = useTranslation();
const {
query: { id },
} = useRouter();
const { data: doc } = useDoc(
{ id: id as string },
{
enabled: !!id,
queryKey: [KEY_DOC_VISIBILITY, { id }],
},
);
if (!doc?.is_public) {
return null;
}
return (
<Text
$weight="bold"
$background={colorsTokens()['primary-600']}
$color="white"
$padding="xtiny"
$radius="3px"
$size="s"
>
{t('Public')}
</Text>
);
};
@@ -9,6 +9,8 @@ import {
ModalShare,
ModalUpdateDoc,
} from '@/features/docs/doc-management';
import { useDocSummaryStore } from '@/features/docs/doc-summary';
import { useDocVersionStore } from '@/features/docs/doc-versioning';
import { ModalPDF } from './ModalExport';
@@ -23,6 +25,8 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
const [isDropOpen, setIsDropOpen] = useState(false);
const { setIsPanelVersionOpen } = useDocVersionStore();
const { setIsPanelSummaryOpen } = useDocSummaryStore();
return (
<Box
@@ -61,7 +65,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
icon={<span className="material-icons">edit</span>}
size="small"
>
<Text $theme="primary">{t('Update document')}</Text>
<Text>{t('Update document')}</Text>
</Button>
)}
{doc.abilities.destroy && (
@@ -74,9 +78,21 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
icon={<span className="material-icons">delete</span>}
size="small"
>
<Text $theme="primary">{t('Delete document')}</Text>
<Text>{t('Delete document')}</Text>
</Button>
)}
<Button
onClick={() => {
setIsPanelSummaryOpen(true);
setIsPanelVersionOpen(false);
setIsDropOpen(false);
}}
color="primary-text"
icon={<span className="material-icons">summarize</span>}
size="small"
>
<Text>{t('Summary')}</Text>
</Button>
<Button
onClick={() => {
setIsModalPDFOpen(true);
@@ -86,7 +102,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
icon={<span className="material-icons">file_download</span>}
size="small"
>
<Text $theme="primary">{t('Export')}</Text>
<Text>{t('Export')}</Text>
</Button>
</Box>
</DropButton>

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