Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93c41f4c12 | |||
| a9d7d53d90 | |||
| aebbc46e79 | |||
| 3770a7f988 | |||
| 54d063ab2c | |||
| 70e4518d76 | |||
| ca54fa11f0 | |||
| 38732db8a4 | |||
| c7439be1a5 | |||
| 152e7b1426 | |||
| 49c89da2da |
+139
-139
@@ -9,52 +9,52 @@ on:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
lint-git:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request' # Makes sense only for pull requests
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: show
|
||||
run: git log
|
||||
- name: Enforce absence of print statements in code
|
||||
run: |
|
||||
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/impress.yml' | grep "print("
|
||||
- name: Check absence of fixup commits
|
||||
run: |
|
||||
! git log | grep 'fixup!'
|
||||
- name: Install gitlint
|
||||
run: pip install --user requests gitlint
|
||||
- name: Lint commit messages added to main
|
||||
run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
|
||||
# lint-git:
|
||||
# runs-on: ubuntu-latest
|
||||
# if: github.event_name == 'pull_request' # Makes sense only for pull requests
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v2
|
||||
# with:
|
||||
# fetch-depth: 0
|
||||
# - name: show
|
||||
# run: git log
|
||||
# - name: Enforce absence of print statements in code
|
||||
# run: |
|
||||
# ! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/impress.yml' | grep "print("
|
||||
# - name: Check absence of fixup commits
|
||||
# run: |
|
||||
# ! git log | grep 'fixup!'
|
||||
# - name: Install gitlint
|
||||
# run: pip install --user requests gitlint
|
||||
# - name: Lint commit messages added to main
|
||||
# run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
|
||||
|
||||
check-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false &&
|
||||
github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Check that the CHANGELOG has been modified in the current branch
|
||||
run: git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.after }} | grep 'CHANGELOG.md'
|
||||
# check-changelog:
|
||||
# runs-on: ubuntu-latest
|
||||
# if: |
|
||||
# contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false &&
|
||||
# github.event_name == 'pull_request'
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v3
|
||||
# with:
|
||||
# fetch-depth: 50
|
||||
# - name: Check that the CHANGELOG has been modified in the current branch
|
||||
# run: git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.after }} | grep 'CHANGELOG.md'
|
||||
|
||||
lint-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Check CHANGELOG max line length
|
||||
run: |
|
||||
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
|
||||
if [ $max_line_length -ge 80 ]; then
|
||||
echo "ERROR: CHANGELOG has lines longer than 80 characters."
|
||||
exit 1
|
||||
fi
|
||||
# lint-changelog:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v2
|
||||
# - name: Check CHANGELOG max line length
|
||||
# run: |
|
||||
# max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
|
||||
# if [ $max_line_length -ge 80 ]; then
|
||||
# echo "ERROR: CHANGELOG has lines longer than 80 characters."
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
build-mails:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -96,112 +96,112 @@ jobs:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
|
||||
lint-back:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/backend
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
- name: Check code formatting with ruff
|
||||
run: ~/.local/bin/ruff format . --diff
|
||||
- name: Lint code with ruff
|
||||
run: ~/.local/bin/ruff check .
|
||||
- name: Lint code with pylint
|
||||
run: ~/.local/bin/pylint .
|
||||
# lint-back:
|
||||
# runs-on: ubuntu-latest
|
||||
# defaults:
|
||||
# run:
|
||||
# working-directory: src/backend
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v2
|
||||
# - name: Install Python
|
||||
# uses: actions/setup-python@v3
|
||||
# with:
|
||||
# python-version: "3.10"
|
||||
# - name: Install development dependencies
|
||||
# run: pip install --user .[dev]
|
||||
# - name: Check code formatting with ruff
|
||||
# run: ~/.local/bin/ruff format . --diff
|
||||
# - name: Lint code with ruff
|
||||
# run: ~/.local/bin/ruff check .
|
||||
# - name: Lint code with pylint
|
||||
# run: ~/.local/bin/pylint .
|
||||
|
||||
test-back:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-mails
|
||||
# test-back:
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: build-mails
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/backend
|
||||
# defaults:
|
||||
# run:
|
||||
# working-directory: src/backend
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_DB: impress
|
||||
POSTGRES_USER: dinum
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5432:5432
|
||||
# needed because the postgres container does not provide a healthcheck
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
# services:
|
||||
# postgres:
|
||||
# image: postgres:16
|
||||
# env:
|
||||
# POSTGRES_DB: impress
|
||||
# POSTGRES_USER: dinum
|
||||
# POSTGRES_PASSWORD: pass
|
||||
# ports:
|
||||
# - 5432:5432
|
||||
# # needed because the postgres container does not provide a healthcheck
|
||||
# options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
|
||||
env:
|
||||
DJANGO_CONFIGURATION: Test
|
||||
DJANGO_SETTINGS_MODULE: impress.settings
|
||||
DJANGO_SECRET_KEY: ThisIsAnExampleKeyForTestPurposeOnly
|
||||
OIDC_OP_JWKS_ENDPOINT: /endpoint-for-test-purpose-only
|
||||
DB_HOST: localhost
|
||||
DB_NAME: impress
|
||||
DB_USER: dinum
|
||||
DB_PASSWORD: pass
|
||||
DB_PORT: 5432
|
||||
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
||||
AWS_S3_ENDPOINT_URL: http://localhost:9000
|
||||
AWS_S3_ACCESS_KEY_ID: impress
|
||||
AWS_S3_SECRET_ACCESS_KEY: password
|
||||
# env:
|
||||
# DJANGO_CONFIGURATION: Test
|
||||
# DJANGO_SETTINGS_MODULE: impress.settings
|
||||
# DJANGO_SECRET_KEY: ThisIsAnExampleKeyForTestPurposeOnly
|
||||
# OIDC_OP_JWKS_ENDPOINT: /endpoint-for-test-purpose-only
|
||||
# DB_HOST: localhost
|
||||
# DB_NAME: impress
|
||||
# DB_USER: dinum
|
||||
# DB_PASSWORD: pass
|
||||
# DB_PORT: 5432
|
||||
# STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
||||
# AWS_S3_ENDPOINT_URL: http://localhost:9000
|
||||
# AWS_S3_ACCESS_KEY_ID: impress
|
||||
# AWS_S3_SECRET_ACCESS_KEY: password
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v4
|
||||
|
||||
- name: Create writable /data
|
||||
run: |
|
||||
sudo mkdir -p /data/media && \
|
||||
sudo mkdir -p /data/static
|
||||
# - name: Create writable /data
|
||||
# run: |
|
||||
# sudo mkdir -p /data/media && \
|
||||
# sudo mkdir -p /data/static
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@v4
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
# - name: Restore the mail templates
|
||||
# uses: actions/cache@v4
|
||||
# id: mail-templates
|
||||
# with:
|
||||
# path: "src/backend/core/templates/mail"
|
||||
# key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
|
||||
- name: Start Minio
|
||||
run: |
|
||||
docker pull minio/minio
|
||||
docker run -d --name minio \
|
||||
-p 9000:9000 \
|
||||
-e "MINIO_ACCESS_KEY=impress" \
|
||||
-e "MINIO_SECRET_KEY=password" \
|
||||
-v /data/media:/data \
|
||||
minio/minio server --console-address :9001 /data
|
||||
# - name: Start Minio
|
||||
# run: |
|
||||
# docker pull minio/minio
|
||||
# docker run -d --name minio \
|
||||
# -p 9000:9000 \
|
||||
# -e "MINIO_ACCESS_KEY=impress" \
|
||||
# -e "MINIO_SECRET_KEY=password" \
|
||||
# -v /data/media:/data \
|
||||
# minio/minio server --console-address :9001 /data
|
||||
|
||||
- name: Configure MinIO
|
||||
run: |
|
||||
MINIO=$(docker ps | grep minio/minio | sed -E 's/.*\s+([a-zA-Z0-9_-]+)$/\1/')
|
||||
docker exec ${MINIO} sh -c \
|
||||
"mc alias set impress http://localhost:9000 impress password && \
|
||||
mc alias ls && \
|
||||
mc mb impress/impress-media-storage && \
|
||||
mc version enable impress/impress-media-storage"
|
||||
# - name: Configure MinIO
|
||||
# run: |
|
||||
# MINIO=$(docker ps | grep minio/minio | sed -E 's/.*\s+([a-zA-Z0-9_-]+)$/\1/')
|
||||
# docker exec ${MINIO} sh -c \
|
||||
# "mc alias set impress http://localhost:9000 impress password && \
|
||||
# mc alias ls && \
|
||||
# mc mb impress/impress-media-storage && \
|
||||
# mc version enable impress/impress-media-storage"
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
# - name: Install Python
|
||||
# uses: actions/setup-python@v3
|
||||
# with:
|
||||
# python-version: "3.10"
|
||||
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
# - name: Install development dependencies
|
||||
# run: pip install --user .[dev]
|
||||
|
||||
- name: Install gettext (required to compile messages)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gettext pandoc
|
||||
# - name: Install gettext (required to compile messages)
|
||||
# run: |
|
||||
# sudo apt-get update
|
||||
# sudo apt-get install -y gettext pandoc
|
||||
|
||||
- name: Generate a MO file from strings extracted from the project
|
||||
run: python manage.py compilemessages
|
||||
# - name: Generate a MO file from strings extracted from the project
|
||||
# run: python manage.py compilemessages
|
||||
|
||||
- name: Run tests
|
||||
run: ~/.local/bin/pytest -n 2
|
||||
# - name: Run tests
|
||||
# run: ~/.local/bin/pytest -n 2
|
||||
|
||||
+3
-12
@@ -11,27 +11,19 @@ and this project adheres to
|
||||
|
||||
## Added
|
||||
|
||||
- ⚗️(backend) Extract text from base64 yjs document #270
|
||||
|
||||
|
||||
## [1.4.0] - 2024-09-17
|
||||
|
||||
## Added
|
||||
|
||||
- ✨Add link public/authenticated/restricted access with read/editor roles #234
|
||||
- ✨(frontend) add copy link button #235
|
||||
- 🛂(frontend) access public docs without being logged #235
|
||||
|
||||
## Changed
|
||||
|
||||
- ♻️(backend) Allow null titles on documents for easier creation #234
|
||||
- ♻️ Allow null titles on documents for easier creation #234
|
||||
- 🛂(backend) stop to list public doc to everyone #234
|
||||
- 🚚(frontend) change visibility in share modal #235
|
||||
- ⚡️(frontend) Improve summary #244
|
||||
|
||||
## Fixed
|
||||
|
||||
- 🐛(backend) Fix forcing ID when creating a document via API endpoint #234
|
||||
- 🐛 Fix forcing ID when creating a document via API endpoint #234
|
||||
- 🐛 Rebuild frontend dev container from makefile #248
|
||||
|
||||
|
||||
@@ -157,8 +149,7 @@ and this project adheres to
|
||||
- 🚀 Impress, project to manage your documents easily and collaboratively.
|
||||
|
||||
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v1.4.0...main
|
||||
[1.4.0]: https://github.com/numerique-gouv/impress/releases/v1.4.0
|
||||
[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
|
||||
|
||||
@@ -16,8 +16,8 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["id", "email"]
|
||||
read_only_fields = ["id", "email"]
|
||||
fields = ["id", "sub", "email"]
|
||||
read_only_fields = ["id", "sub", "email"]
|
||||
|
||||
|
||||
class BaseAccessSerializer(serializers.ModelSerializer):
|
||||
@@ -148,6 +148,7 @@ class DocumentSerializer(BaseResourceSerializer):
|
||||
"title",
|
||||
"accesses",
|
||||
"abilities",
|
||||
"is_e2ee",
|
||||
"link_role",
|
||||
"link_reach",
|
||||
"created_at",
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1 on 2024-09-12 13:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_remove_document_is_public_alter_document_link_reach_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
name='is_e2ee',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -329,6 +329,7 @@ class Document(BaseModel):
|
||||
link_role = models.CharField(
|
||||
max_length=20, choices=LinkRoleChoices.choices, default=LinkRoleChoices.READER
|
||||
)
|
||||
is_e2ee = models.BooleanField(default=False)
|
||||
|
||||
_content = None
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ def test_api_documents_retrieve_anonymous_public():
|
||||
"versions_retrieve": False,
|
||||
},
|
||||
"accesses": [],
|
||||
"is_e2ee": False,
|
||||
"link_reach": "public",
|
||||
"link_role": document.link_role,
|
||||
"title": document.title,
|
||||
@@ -87,6 +88,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
||||
"versions_retrieve": False,
|
||||
},
|
||||
"accesses": [],
|
||||
"is_e2ee": False,
|
||||
"link_reach": reach,
|
||||
"link_role": document.link_role,
|
||||
"title": document.title,
|
||||
@@ -194,6 +196,7 @@ def test_api_documents_retrieve_authenticated_related_direct():
|
||||
"title": document.title,
|
||||
"content": document.content,
|
||||
"abilities": document.get_abilities(user),
|
||||
"is_e2ee": False,
|
||||
"link_reach": document.link_reach,
|
||||
"link_role": document.link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
@@ -332,6 +335,7 @@ def test_api_documents_retrieve_authenticated_related_team_members(
|
||||
"title": document.title,
|
||||
"content": document.content,
|
||||
"abilities": document.get_abilities(user),
|
||||
"is_e2ee": False,
|
||||
"link_reach": "restricted",
|
||||
"link_role": document.link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
@@ -452,6 +456,7 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
|
||||
"title": document.title,
|
||||
"content": document.content,
|
||||
"abilities": document.get_abilities(user),
|
||||
"is_e2ee": False,
|
||||
"link_reach": "restricted",
|
||||
"link_role": document.link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
@@ -576,6 +581,7 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
||||
"title": document.title,
|
||||
"content": document.content,
|
||||
"abilities": document.get_abilities(user),
|
||||
"is_e2ee": False,
|
||||
"link_reach": "restricted",
|
||||
"link_role": document.link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.core import mail
|
||||
|
||||
import pytest
|
||||
|
||||
from core.utils import email_invitation, yjs_base64_to_text
|
||||
from core.utils import email_invitation
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -85,29 +85,3 @@ def test_utils__email_invitation_failed(mock_logger, _mock_send_mail):
|
||||
|
||||
assert email == "guest@example.com"
|
||||
assert isinstance(exception, smtplib.SMTPException)
|
||||
|
||||
|
||||
def test_yjs_base64_to_text():
|
||||
"""
|
||||
Test extract_text_from_saved_yjs_document
|
||||
This base64 string is an example of what is saved in the database.
|
||||
This base64 is generated from the blocknote editor, it contains
|
||||
the text \n# *Hello* \n- w**or**ld
|
||||
"""
|
||||
base64_string = (
|
||||
"AR717vLVDgAHAQ5kb2N1bWVudC1zdG9yZQMKYmxvY2tHcm91cAcA9e7y1Q4AAw5ibG9ja0NvbnRh"
|
||||
"aW5lcgcA9e7y1Q4BAwdoZWFkaW5nBwD17vLVDgIGBgD17vLVDgMGaXRhbGljAnt9hPXu8tUOBAVI"
|
||||
"ZWxsb4b17vLVDgkGaXRhbGljBG51bGwoAPXu8tUOAg10ZXh0QWxpZ25tZW50AXcEbGVmdCgA9e7y"
|
||||
"1Q4CBWxldmVsAX0BKAD17vLVDgECaWQBdyQwNGQ2MjM0MS04MzI2LTQyMzYtYTA4My00ODdlMjZm"
|
||||
"YWQyMzAoAPXu8tUOAQl0ZXh0Q29sb3IBdwdkZWZhdWx0KAD17vLVDgEPYmFja2dyb3VuZENvbG9y"
|
||||
"AXcHZGVmYXVsdIf17vLVDgEDDmJsb2NrQ29udGFpbmVyBwD17vLVDhADDmJ1bGxldExpc3RJdGVt"
|
||||
"BwD17vLVDhEGBAD17vLVDhIBd4b17vLVDhMEYm9sZAJ7fYT17vLVDhQCb3KG9e7y1Q4WBGJvbGQE"
|
||||
"bnVsbIT17vLVDhcCbGQoAPXu8tUOEQ10ZXh0QWxpZ25tZW50AXcEbGVmdCgA9e7y1Q4QAmlkAXck"
|
||||
"ZDM1MWUwNjgtM2U1NS00MjI2LThlYTUtYWJiMjYzMTk4ZTJhKAD17vLVDhAJdGV4dENvbG9yAXcH"
|
||||
"ZGVmYXVsdCgA9e7y1Q4QD2JhY2tncm91bmRDb2xvcgF3B2RlZmF1bHSH9e7y1Q4QAw5ibG9ja0Nv"
|
||||
"bnRhaW5lcgcA9e7y1Q4eAwlwYXJhZ3JhcGgoAPXu8tUOHw10ZXh0QWxpZ25tZW50AXcEbGVmdCgA"
|
||||
"9e7y1Q4eAmlkAXckODk3MDBjMDctZTBlMS00ZmUwLWFjYTItODQ5MzIwOWE3ZTQyKAD17vLVDh4J"
|
||||
"dGV4dENvbG9yAXcHZGVmYXVsdCgA9e7y1Q4eD2JhY2tncm91bmRDb2xvcgF3B2RlZmF1bHQA"
|
||||
)
|
||||
|
||||
assert yjs_base64_to_text(base64_string) == "Hello world"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Utilities for the core app.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import smtplib
|
||||
from logging import getLogger
|
||||
|
||||
@@ -13,9 +12,6 @@ from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import override
|
||||
|
||||
import y_py as Y
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
@@ -42,17 +38,3 @@ def email_invitation(language, email, document_id):
|
||||
|
||||
except smtplib.SMTPException as exception:
|
||||
logger.error("invitation to %s was not sent: %s", email, exception)
|
||||
|
||||
|
||||
def yjs_base64_to_text(base64_string):
|
||||
"""Extract text from base64 yjs document"""
|
||||
|
||||
decoded_bytes = base64.b64decode(base64_string)
|
||||
uint8_array = bytearray(decoded_bytes)
|
||||
|
||||
doc = Y.YDoc() # pylint: disable=E1101
|
||||
Y.apply_update(doc, uint8_array) # pylint: disable=E1101
|
||||
blocknote_structure = str(doc.get_xml_element("document-store"))
|
||||
|
||||
soup = BeautifulSoup(blocknote_structure, "html.parser")
|
||||
return soup.get_text(separator=" ").strip()
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "1.4.0"
|
||||
version = "1.3.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -25,7 +25,6 @@ license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"beautifulsoup4==4.12.3",
|
||||
"boto3==1.35.10",
|
||||
"Brotli==1.1.0",
|
||||
"celery[redis]==5.4.0",
|
||||
@@ -58,7 +57,6 @@ dependencies = [
|
||||
"WeasyPrint>=60.2",
|
||||
"whitenoise==6.7.0",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
"y-py==0.5.5",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Doc Table Content', () => {
|
||||
test('it checks the doc table content', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(
|
||||
page,
|
||||
'doc-table-content',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Table of contents',
|
||||
})
|
||||
.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 editor.getByText('Hello').dblclick();
|
||||
await page.getByRole('button', { name: 'Strike' }).click();
|
||||
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
|
||||
// Create space to fill the viewport
|
||||
for (let i = 0; i < 5; 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', { delay: 100 });
|
||||
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
|
||||
// Create space to fill the viewport
|
||||
for (let i = 0; i < 5; 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');
|
||||
|
||||
const hello = panel.getByText('Hello World');
|
||||
const superW = panel.getByText('Super World');
|
||||
const another = panel.getByText('Another World');
|
||||
|
||||
await expect(hello).toBeVisible();
|
||||
await expect(hello).toHaveCSS('font-size', /19/);
|
||||
await expect(hello).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
await expect(superW).toBeVisible();
|
||||
await expect(superW).toHaveCSS('font-size', /16/);
|
||||
await expect(superW).toHaveAttribute('aria-selected', 'false');
|
||||
|
||||
await expect(another).toBeVisible();
|
||||
await expect(another).toHaveCSS('font-size', /12/);
|
||||
await expect(another).toHaveAttribute('aria-selected', 'false');
|
||||
|
||||
await hello.click();
|
||||
|
||||
await expect(editor.getByText('Hello World')).toBeInViewport();
|
||||
await expect(hello).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(superW).toHaveAttribute('aria-selected', 'false');
|
||||
|
||||
await another.click();
|
||||
|
||||
await expect(editor.getByText('Hello World')).not.toBeInViewport();
|
||||
await expect(hello).toHaveAttribute('aria-selected', 'false');
|
||||
await expect(superW).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
await panel.getByText('Back to top').click();
|
||||
await expect(editor.getByText('Hello World')).toBeInViewport();
|
||||
await expect(hello).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(superW).toHaveAttribute('aria-selected', 'false');
|
||||
|
||||
await panel.getByText('Go to bottom').click();
|
||||
await expect(editor.getByText('Hello World')).not.toBeInViewport();
|
||||
await expect(superW).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "1.4.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.47.1",
|
||||
"@playwright/test": "1.47.0",
|
||||
"@types/node": "*",
|
||||
"@types/pdf-parse": "1.1.4",
|
||||
"eslint-config-impress": "*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "1.4.0",
|
||||
"version": "1.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -21,16 +21,19 @@
|
||||
"@gouvfr-lasuite/integration": "1.0.2",
|
||||
"@hocuspocus/provider": "2.13.5",
|
||||
"@openfun/cunningham-react": "2.9.4",
|
||||
"@tanstack/react-query": "5.56.2",
|
||||
"@socialgouv/e2esdk-client": "1.0.0-beta.28",
|
||||
"@socialgouv/e2esdk-devtools": "1.0.0-beta.38",
|
||||
"@socialgouv/e2esdk-react": "1.0.0-beta.28",
|
||||
"@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.11",
|
||||
"next": "14.2.9",
|
||||
"react": "*",
|
||||
"react-aria-components": "1.3.3",
|
||||
"react-dom": "*",
|
||||
"react-i18next": "15.0.2",
|
||||
"react-i18next": "15.0.1",
|
||||
"react-select": "5.8.0",
|
||||
"styled-components": "6.1.13",
|
||||
"y-protocols": "1.0.6",
|
||||
@@ -39,16 +42,16 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@tanstack/react-query-devtools": "5.56.2",
|
||||
"@tanstack/react-query-devtools": "5.55.4",
|
||||
"@testing-library/dom": "10.4.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.13",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/lodash": "4.17.7",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/node": "*",
|
||||
"@types/react": "18.3.6",
|
||||
"@types/react": "18.3.5",
|
||||
"@types/react-dom": "*",
|
||||
"cross-env": "*",
|
||||
"dotenv": "16.4.5",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Box, Card, IconBG, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
interface PanelProps {
|
||||
title?: string;
|
||||
title: string;
|
||||
setIsPanelOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -36,10 +36,10 @@ export const Panel = ({
|
||||
$width="100%"
|
||||
$maxWidth="20rem"
|
||||
$position="sticky"
|
||||
$maxHeight="99vh"
|
||||
$maxHeight="96vh"
|
||||
$height="100%"
|
||||
$css={`
|
||||
top: 0vh;
|
||||
top: 2vh;
|
||||
transition: ${transition};
|
||||
${
|
||||
!isOpen &&
|
||||
@@ -53,14 +53,11 @@ export const Panel = ({
|
||||
{...closedOverridingStyles}
|
||||
>
|
||||
<Box
|
||||
$overflow="inherit"
|
||||
$position="sticky"
|
||||
$overflow="hidden"
|
||||
$css={`
|
||||
top: 0;
|
||||
opacity: ${isOpen ? '1' : '0'};
|
||||
transition: ${transition};
|
||||
`}
|
||||
$maxHeight="100%"
|
||||
>
|
||||
<Box
|
||||
$padding={{ all: 'small' }}
|
||||
@@ -93,11 +90,9 @@ export const Panel = ({
|
||||
}}
|
||||
$radius="2px"
|
||||
/>
|
||||
{title && (
|
||||
<Text $weight="bold" $size="l" $theme="primary">
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
<Text $weight="bold" $size="l" $theme="primary">
|
||||
{title}
|
||||
</Text>
|
||||
</Box>
|
||||
{children}
|
||||
</Box>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { CunninghamProvider } from '@openfun/cunningham-react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { E2ESDKClientProvider } from '@socialgouv/e2esdk-react';
|
||||
import { e2esdkClient } from './auth/useAuthStore';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import '@/i18n/initI18n';
|
||||
@@ -27,7 +29,9 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CunninghamProvider theme={theme}>
|
||||
<Auth>{children}</Auth>
|
||||
<E2ESDKClientProvider client={e2esdkClient}>
|
||||
<Auth>{children}</Auth>
|
||||
</E2ESDKClientProvider>
|
||||
</CunninghamProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
* Represents user retrieved from the API.
|
||||
* @interface User
|
||||
* @property {string} id - The id of the user.
|
||||
* @property {string} sub - The `sub` field of OIDC
|
||||
* @property {string} email - The email of the user.
|
||||
* @property {string} name - The name of the user.
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
sub: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
@@ -5,18 +5,31 @@ import { baseApiUrl } from '@/core/conf';
|
||||
import { User, getMe } from './api';
|
||||
import { PATH_AUTH_LOCAL_STORAGE } from './conf';
|
||||
|
||||
import { Client, PublicUserIdentity } from '@socialgouv/e2esdk-client';
|
||||
import { identity } from 'lodash';
|
||||
|
||||
export const e2esdkClient = new Client({
|
||||
// Point it to where your server is listening
|
||||
serverURL: 'https://app-a5a1b445-32e0-4cf4-a478-821a48f86ccf.cleverapps.io',
|
||||
// Pass the signature public key you configured for the server
|
||||
serverSignaturePublicKey: 'ayfva9SUh0mfgmifUtxcdLp4HriHJiqefEKnvYgY4qM',
|
||||
});
|
||||
|
||||
interface AuthStore {
|
||||
initiated: boolean;
|
||||
authenticated: boolean;
|
||||
readyForEncryption: boolean;
|
||||
initAuth: () => void;
|
||||
logout: () => void;
|
||||
login: () => void;
|
||||
endToEndData?: PublicUserIdentity;
|
||||
userData?: User;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
initiated: false,
|
||||
authenticated: false,
|
||||
readyForEncryption: false,
|
||||
userData: undefined,
|
||||
};
|
||||
|
||||
@@ -24,22 +37,51 @@ export const useAuthStore = create<AuthStore>((set) => ({
|
||||
initiated: initialState.initiated,
|
||||
authenticated: initialState.authenticated,
|
||||
userData: initialState.userData,
|
||||
readyForEncryption: initialState.readyForEncryption,
|
||||
|
||||
initAuth: () => {
|
||||
getMe()
|
||||
.then((data: User) => {
|
||||
// If a path is stored in the local storage, we redirect to it
|
||||
const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
|
||||
if (path_auth) {
|
||||
localStorage.removeItem(PATH_AUTH_LOCAL_STORAGE);
|
||||
window.location.replace(path_auth);
|
||||
return;
|
||||
}
|
||||
.then(
|
||||
(data: User) => {
|
||||
// If a path is stored in the local storage, we redirect to it
|
||||
const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
|
||||
if (path_auth) {
|
||||
localStorage.removeItem(PATH_AUTH_LOCAL_STORAGE);
|
||||
window.location.replace(path_auth);
|
||||
return;
|
||||
}
|
||||
|
||||
set({ authenticated: true, userData: data });
|
||||
set({ authenticated: true, userData: data });
|
||||
return e2esdkClient
|
||||
.signup(data.sub)
|
||||
.then(() => data)
|
||||
.catch(() => data);
|
||||
},
|
||||
() => {},
|
||||
)
|
||||
.then(
|
||||
(data) => {
|
||||
set({ readyForEncryption: true });
|
||||
if (data) {
|
||||
return e2esdkClient.login(data.sub);
|
||||
}
|
||||
},
|
||||
(e) => {
|
||||
throw e;
|
||||
//if (data) {
|
||||
// return e2esdkClient.login(data.sub);
|
||||
//}
|
||||
//fail
|
||||
},
|
||||
)
|
||||
.then((publicIdentity: PublicUserIdentity | null | undefined) => {
|
||||
if (!publicIdentity) throw Error('exploding');
|
||||
console.log('publicIdentity', publicIdentity);
|
||||
set({ endToEndData: publicIdentity });
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
console.log('finally');
|
||||
set({ initiated: true });
|
||||
});
|
||||
},
|
||||
|
||||
+41
-11
@@ -1,4 +1,4 @@
|
||||
import { BlockNoteEditor as BlockNoteEditorCore } from '@blocknote/core';
|
||||
import { Block, BlockNoteEditor as BlockNoteEditorCore, PartialBlock } from '@blocknote/core';
|
||||
import '@blocknote/core/fonts/inter.css';
|
||||
import { BlockNoteView } from '@blocknote/mantine';
|
||||
import '@blocknote/mantine/style.css';
|
||||
@@ -17,6 +17,7 @@ import { useDocStore } from '../stores';
|
||||
import { randomColor } from '../utils';
|
||||
|
||||
import { BlockNoteToolbar } from './BlockNoteToolbar';
|
||||
import { useE2ESDKClient } from '@socialgouv/e2esdk-react';
|
||||
|
||||
const cssEditor = `
|
||||
&, & > .bn-container, & .ProseMirror {
|
||||
@@ -71,7 +72,8 @@ export const BlockNoteContent = ({
|
||||
const { userData } = useAuthStore();
|
||||
const { setStore, docsStore } = useDocStore();
|
||||
const canSave = doc.abilities.partial_update && !isVersion;
|
||||
useSaveDoc(doc.id, provider.document, canSave);
|
||||
|
||||
const e2eClient = useE2ESDKClient();
|
||||
const storedEditor = docsStore?.[storeId]?.editor;
|
||||
const {
|
||||
mutateAsync: createDocAttachment,
|
||||
@@ -99,18 +101,46 @@ export const BlockNoteContent = ({
|
||||
return storedEditor;
|
||||
}
|
||||
|
||||
// TODO decrypt doc.content
|
||||
//localStorage.getItem('KEY');
|
||||
|
||||
const docId = doc.id;
|
||||
const purpose = `docs:${docId}`;
|
||||
const key = e2eClient.findKeyByPurpose(purpose);
|
||||
console.log('purpose', purpose, 'key', key);
|
||||
let plaintextContent: Array<PartialBlock> | undefined;
|
||||
if (!key) {
|
||||
alert('probleme de key');
|
||||
return;
|
||||
} else {
|
||||
if (doc.content) {
|
||||
plaintextContent = JSON.parse(e2eClient.decrypt(
|
||||
doc.content,
|
||||
key.keychainFingerprint,
|
||||
) as string) as Array<PartialBlock>;
|
||||
|
||||
console.log('decryptedMessage', plaintextContent);
|
||||
} else {
|
||||
plaintextContent = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return BlockNoteEditorCore.create({
|
||||
collaboration: {
|
||||
provider,
|
||||
fragment: provider.document.getXmlFragment('document-store'),
|
||||
user: {
|
||||
name: userData?.email || 'Anonymous',
|
||||
color: randomColor(),
|
||||
},
|
||||
},
|
||||
// collaboration: {
|
||||
// provider,
|
||||
// fragment: provider.document.getXmlFragment('document-store'),
|
||||
// user: {
|
||||
// name: userData?.email || 'Anonymous',
|
||||
// color: randomColor(),
|
||||
// },
|
||||
// },
|
||||
uploadFile,
|
||||
initialContent: plaintextContent,
|
||||
});
|
||||
}, [provider, storedEditor, uploadFile, userData?.email]);
|
||||
}, [doc.content, storedEditor, uploadFile]);
|
||||
|
||||
console.log("useSaveDoc", doc.id, provider.document, canSave, editor);
|
||||
useSaveDoc(doc.id, provider.document, canSave, editor);
|
||||
|
||||
useEffect(() => {
|
||||
setStore(storeId, { editor });
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Panel } from '@/components/Panel';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { DocHeader } from '@/features/docs/doc-header';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import { TableContent } from '@/features/docs/doc-table-content';
|
||||
import { Summary, useDocSummaryStore } from '@/features/docs/doc-summary';
|
||||
import {
|
||||
VersionList,
|
||||
Versions,
|
||||
@@ -28,6 +28,8 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||
query: { versionId },
|
||||
} = useRouter();
|
||||
const { isPanelVersionOpen, setIsPanelVersionOpen } = useDocVersionStore();
|
||||
const { isPanelSummaryOpen, setIsPanelSummaryOpen } = useDocSummaryStore();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isVersion = versionId && typeof versionId === 'string';
|
||||
@@ -70,7 +72,11 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||
<VersionList doc={doc} />
|
||||
</Panel>
|
||||
)}
|
||||
<TableContent doc={doc} />
|
||||
{isPanelSummaryOpen && (
|
||||
<Panel title={t('SUMMARY')} setIsPanelOpen={setIsPanelSummaryOpen}>
|
||||
<Summary doc={doc} />
|
||||
</Panel>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import * as Y from 'yjs';
|
||||
@@ -6,17 +7,24 @@ import { useUpdateDoc } from '@/features/docs/doc-management/';
|
||||
import { KEY_LIST_DOC_VERSIONS } from '@/features/docs/doc-versioning';
|
||||
|
||||
import { toBase64 } from '../utils';
|
||||
import { useE2ESDKClient } from '@socialgouv/e2esdk-react';
|
||||
|
||||
const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
|
||||
const useSaveDoc = (
|
||||
docId: string,
|
||||
doc: Y.Doc,
|
||||
canSave: boolean,
|
||||
editor: BlockNoteEditor,
|
||||
) => {
|
||||
const { mutate: updateDoc } = useUpdateDoc({
|
||||
listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
|
||||
});
|
||||
const e2eClient = useE2ESDKClient();
|
||||
const [initialDoc, setInitialDoc] = useState<string>(
|
||||
toBase64(Y.encodeStateAsUpdate(doc)),
|
||||
JSON.stringify(editor.document)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setInitialDoc(toBase64(Y.encodeStateAsUpdate(doc)));
|
||||
setInitialDoc(JSON.stringify(editor.document));
|
||||
}, [doc]);
|
||||
|
||||
/**
|
||||
@@ -32,7 +40,7 @@ const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
|
||||
transaction: Y.Transaction,
|
||||
) => {
|
||||
if (!transaction.local) {
|
||||
setInitialDoc(toBase64(Y.encodeStateAsUpdate(updatedDoc)));
|
||||
setInitialDoc(JSON.stringify(editor.document));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -47,23 +55,41 @@ const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
|
||||
* Check if the doc has been updated and can be saved.
|
||||
*/
|
||||
const hasChanged = useCallback(() => {
|
||||
const newDoc = toBase64(Y.encodeStateAsUpdate(doc));
|
||||
return initialDoc !== newDoc;
|
||||
return initialDoc !== JSON.stringify(editor.document);
|
||||
}, [doc, initialDoc]);
|
||||
|
||||
const shouldSave = useCallback(() => {
|
||||
console.log('hasChanged', hasChanged(), 'canSave', canSave);
|
||||
return hasChanged() && canSave;
|
||||
}, [canSave, hasChanged]);
|
||||
|
||||
const saveDoc = useCallback(() => {
|
||||
const newDoc = toBase64(Y.encodeStateAsUpdate(doc));
|
||||
const newDoc = JSON.stringify(editor.document);
|
||||
//const newDoc = toBase64(Y.encodeStateAsUpdate(doc));
|
||||
|
||||
// TODO encode the content
|
||||
|
||||
const purpose = `docs:${docId}`;
|
||||
const key = e2eClient.findKeyByPurpose(purpose);
|
||||
console.log("purpose", purpose, "key", key);
|
||||
if (!key) {
|
||||
alert('probleme de key');
|
||||
return;
|
||||
}
|
||||
|
||||
const encrypted = e2eClient.encrypt(newDoc, key.keychainFingerprint);
|
||||
|
||||
console.log('encrypted', encrypted);
|
||||
|
||||
// todo
|
||||
|
||||
setInitialDoc(newDoc);
|
||||
|
||||
updateDoc({
|
||||
id: docId,
|
||||
content: newDoc,
|
||||
content: encrypted,
|
||||
});
|
||||
}, [doc, docId, updateDoc]);
|
||||
}, [docId, editor?.document, updateDoc]);
|
||||
|
||||
const timeout = useRef<NodeJS.Timeout>();
|
||||
const router = useRouter();
|
||||
@@ -74,10 +100,12 @@ const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
|
||||
}
|
||||
|
||||
const onSave = (e?: Event) => {
|
||||
console.log('entered onSave');
|
||||
if (!shouldSave()) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('will save');
|
||||
saveDoc();
|
||||
|
||||
/**
|
||||
@@ -96,7 +124,7 @@ const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
|
||||
};
|
||||
|
||||
// Save every minute
|
||||
timeout.current = setInterval(onSave, 60000);
|
||||
timeout.current = setInterval(onSave, 1000);
|
||||
// Save when the user leaves the page
|
||||
addEventListener('beforeunload', onSave);
|
||||
// Save when the user navigates to another page
|
||||
|
||||
@@ -26,9 +26,9 @@ export const useDocStore = create<UseDocStore>((set, get) => ({
|
||||
guid: storeId,
|
||||
});
|
||||
|
||||
if (initialDoc) {
|
||||
Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64'));
|
||||
}
|
||||
// if (initialDoc) {
|
||||
// Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64'));
|
||||
// }
|
||||
|
||||
const provider = new HocuspocusProvider({
|
||||
url: providerUrl(storeId),
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
ModalShare,
|
||||
ModalUpdateDoc,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { useDocTableContentStore } from '@/features/docs/doc-table-content';
|
||||
import { useDocSummaryStore } from '@/features/docs/doc-summary';
|
||||
import { useDocVersionStore } from '@/features/docs/doc-versioning';
|
||||
|
||||
import { ModalPDF } from './ModalExport';
|
||||
@@ -26,7 +26,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
|
||||
const [isDropOpen, setIsDropOpen] = useState(false);
|
||||
const { setIsPanelVersionOpen } = useDocVersionStore();
|
||||
const { setIsPanelTableContentOpen } = useDocTableContentStore();
|
||||
const { setIsPanelSummaryOpen } = useDocSummaryStore();
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -83,7 +83,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsPanelTableContentOpen(true);
|
||||
setIsPanelSummaryOpen(true);
|
||||
setIsPanelVersionOpen(false);
|
||||
setIsDropOpen(false);
|
||||
}}
|
||||
@@ -91,7 +91,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
icon={<span className="material-icons">summarize</span>}
|
||||
size="small"
|
||||
>
|
||||
<Text $theme="primary">{t('Table of contents')}</Text>
|
||||
<Text $theme="primary">{t('Summary')}</Text>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { e2esdkClient } from '@/core/auth/useAuthStore';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
@@ -13,6 +14,7 @@ export const createDoc = async ({ title }: CreateDocParam): Promise<Doc> => {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
is_e2ee: true
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -20,7 +22,16 @@ export const createDoc = async ({ title }: CreateDocParam): Promise<Doc> => {
|
||||
throw new APIError('Failed to create the doc', await errorCauses(response));
|
||||
}
|
||||
|
||||
return response.json() as Promise<Doc>;
|
||||
const resp = await (response.json() as Promise<Doc>);
|
||||
|
||||
const { keychainFingerprint } = await e2esdkClient.createNewKeychain(
|
||||
`docs:${resp.id}`,
|
||||
'secretBox'
|
||||
);
|
||||
|
||||
console.log('new e2ee keychain registered', keychainFingerprint);
|
||||
|
||||
return resp;
|
||||
};
|
||||
|
||||
interface CreateDocProps {
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface Doc {
|
||||
accesses: Access[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
is_e2ee: boolean;
|
||||
abilities: {
|
||||
destroy: boolean;
|
||||
link_configuration: boolean;
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, BoxButton, Text } from '@/components';
|
||||
|
||||
import { useDocStore } from '../../doc-editor';
|
||||
import { Doc } from '../../doc-management';
|
||||
import { useDocSummaryStore } from '../stores';
|
||||
|
||||
interface SummaryProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const Summary = ({ doc }: SummaryProps) => {
|
||||
const { docsStore } = useDocStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const editor = docsStore?.[doc.id]?.editor;
|
||||
const headingFiltering = useCallback(
|
||||
() => editor?.document.filter((block) => block.type === 'heading'),
|
||||
[editor?.document],
|
||||
);
|
||||
|
||||
const [headings, setHeadings] = useState(headingFiltering());
|
||||
const { setIsPanelSummaryOpen } = useDocSummaryStore();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setIsPanelSummaryOpen(false);
|
||||
};
|
||||
}, [setIsPanelSummaryOpen]);
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
editor.onEditorContentChange(() => {
|
||||
setHeadings(headingFiltering());
|
||||
});
|
||||
|
||||
return (
|
||||
<Box $overflow="auto" $padding="small">
|
||||
{headings?.map((heading) => (
|
||||
<BoxButton
|
||||
key={heading.id}
|
||||
onClick={() => {
|
||||
editor.focus();
|
||||
editor?.setTextCursorPosition(heading.id, 'end');
|
||||
document
|
||||
.querySelector(`[data-id="${heading.id}"]`)
|
||||
?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
}}
|
||||
style={{ textAlign: 'left' }}
|
||||
>
|
||||
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
|
||||
{heading.content?.[0]?.type === 'text' && heading.content?.[0]?.text
|
||||
? `- ${heading.content[0].text}`
|
||||
: ''}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
))}
|
||||
<Box
|
||||
$height="1px"
|
||||
$width="auto"
|
||||
$background="#e5e5e5"
|
||||
$margin={{ vertical: 'small' }}
|
||||
$css="flex: none;"
|
||||
/>
|
||||
<BoxButton
|
||||
onClick={() => {
|
||||
editor.focus();
|
||||
document.querySelector(`.bn-editor`)?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
|
||||
{t('Back to top')}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
<BoxButton
|
||||
onClick={() => {
|
||||
editor.focus();
|
||||
document
|
||||
.querySelector(
|
||||
`.bn-editor > .bn-block-group > .bn-block-outer:last-child`,
|
||||
)
|
||||
?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
|
||||
{t('Go to bottom')}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Summary';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './useDocSummaryStore';
|
||||
@@ -0,0 +1,13 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface UseDocSummaryStore {
|
||||
isPanelSummaryOpen: boolean;
|
||||
setIsPanelSummaryOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const useDocSummaryStore = create<UseDocSummaryStore>((set) => ({
|
||||
isPanelSummaryOpen: false,
|
||||
setIsPanelSummaryOpen: (isPanelSummaryOpen) => {
|
||||
set(() => ({ isPanelSummaryOpen }));
|
||||
},
|
||||
}));
|
||||
@@ -1,67 +0,0 @@
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { BoxButton, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
const sizeMap: { [key: number]: string } = {
|
||||
1: '1.2rem',
|
||||
2: '1rem',
|
||||
3: '0.8rem',
|
||||
};
|
||||
|
||||
export type HeadingsHighlight = {
|
||||
headingId: string;
|
||||
isVisible: boolean;
|
||||
}[];
|
||||
|
||||
interface HeadingProps {
|
||||
editor: BlockNoteEditor;
|
||||
level: number;
|
||||
text: string;
|
||||
headingId: string;
|
||||
isHighlight: boolean;
|
||||
}
|
||||
|
||||
export const Heading = ({
|
||||
headingId,
|
||||
editor,
|
||||
isHighlight,
|
||||
level,
|
||||
text,
|
||||
}: HeadingProps) => {
|
||||
const [isHover, setIsHover] = useState(isHighlight);
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<BoxButton
|
||||
key={headingId}
|
||||
onMouseOver={() => setIsHover(true)}
|
||||
onMouseLeave={() => setIsHover(false)}
|
||||
onClick={() => {
|
||||
editor.focus();
|
||||
editor.setTextCursorPosition(headingId, 'end');
|
||||
document.querySelector(`[data-id="${headingId}"]`)?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
}}
|
||||
$css="text-align: left;"
|
||||
>
|
||||
<Text
|
||||
$theme="primary"
|
||||
$padding={{ vertical: 'xtiny', left: 'tiny' }}
|
||||
$size={sizeMap[level]}
|
||||
$hasTransition
|
||||
$css={
|
||||
isHover || isHighlight
|
||||
? `box-shadow: -2px 0px 0px ${colorsTokens()[isHighlight ? 'primary-500' : 'primary-400']};`
|
||||
: ''
|
||||
}
|
||||
aria-selected={isHighlight}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
);
|
||||
};
|
||||
-197
@@ -1,197 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, BoxButton, Text } from '@/components';
|
||||
import { Panel } from '@/components/Panel';
|
||||
import { useDocStore } from '@/features/docs/doc-editor';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
|
||||
import { useDocTableContentStore } from '../stores';
|
||||
|
||||
import { Heading } from './Heading';
|
||||
|
||||
const recursiveTextContent = (content: HeadingBlock['content']): string => {
|
||||
if (!content) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return content.reduce((acc, content) => {
|
||||
if (content.type === 'text') {
|
||||
return acc + content.text;
|
||||
} else if (content.type === 'link') {
|
||||
return acc + recursiveTextContent(content.content);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, '');
|
||||
};
|
||||
|
||||
type HeadingBlock = {
|
||||
id: string;
|
||||
type: string;
|
||||
text: string;
|
||||
content: HeadingBlock[];
|
||||
contentText: string;
|
||||
props: {
|
||||
level: number;
|
||||
};
|
||||
};
|
||||
|
||||
interface TableContentProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const TableContent = ({ doc }: TableContentProps) => {
|
||||
const { docsStore } = useDocStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const editor = docsStore?.[doc.id]?.editor;
|
||||
const headingFiltering = useCallback(
|
||||
() =>
|
||||
editor?.document
|
||||
.filter((block) => block.type === 'heading')
|
||||
.map((block) => ({
|
||||
...block,
|
||||
contentText: recursiveTextContent(
|
||||
block.content as unknown as HeadingBlock['content'],
|
||||
),
|
||||
})) as unknown as HeadingBlock[],
|
||||
[editor?.document],
|
||||
);
|
||||
|
||||
const [headings, setHeadings] = useState<HeadingBlock[]>();
|
||||
const { setIsPanelTableContentOpen, isPanelTableContentOpen } =
|
||||
useDocTableContentStore();
|
||||
const [hasBeenClose, setHasBeenClose] = useState(false);
|
||||
const setClosePanel = () => {
|
||||
setHasBeenClose(true);
|
||||
setIsPanelTableContentOpen(false);
|
||||
};
|
||||
|
||||
const [headingIdHighlight, setHeadingIdHighlight] = useState<string>();
|
||||
|
||||
// Open the panel if there are more than 1 heading
|
||||
useEffect(() => {
|
||||
if (headings?.length && headings.length > 1 && !hasBeenClose) {
|
||||
setIsPanelTableContentOpen(true);
|
||||
}
|
||||
}, [setIsPanelTableContentOpen, headings, hasBeenClose]);
|
||||
|
||||
// Close the panel unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setIsPanelTableContentOpen(false);
|
||||
};
|
||||
}, [setIsPanelTableContentOpen]);
|
||||
|
||||
// To highlight the first heading in the viewport
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (!headings) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const heading of headings) {
|
||||
const elHeading = document.body.querySelector(
|
||||
`.bn-block-outer[data-id="${heading.id}"] [data-content-type="heading"]:first-child`,
|
||||
);
|
||||
|
||||
if (!elHeading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = elHeading.getBoundingClientRect();
|
||||
const isVisible =
|
||||
rect.top + rect.height >= 1 &&
|
||||
rect.bottom <=
|
||||
(window.innerHeight || document.documentElement.clientHeight);
|
||||
|
||||
if (isVisible) {
|
||||
setHeadingIdHighlight(heading.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
setTimeout(() => {
|
||||
handleScroll();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [headings, setHeadingIdHighlight]);
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update the headings when the editor content changes
|
||||
editor?.onEditorContentChange(() => {
|
||||
setHeadings(headingFiltering());
|
||||
});
|
||||
|
||||
if (!isPanelTableContentOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel setIsPanelOpen={setClosePanel}>
|
||||
<Box $padding="small" $maxHeight="95%">
|
||||
<Box $overflow="auto">
|
||||
{headings?.map((heading) => (
|
||||
<Heading
|
||||
editor={editor}
|
||||
headingId={heading.id}
|
||||
level={heading.props.level}
|
||||
text={heading.contentText}
|
||||
key={heading.id}
|
||||
isHighlight={headingIdHighlight === heading.id}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<Box
|
||||
$height="1px"
|
||||
$width="auto"
|
||||
$background="#e5e5e5"
|
||||
$margin={{ vertical: 'small' }}
|
||||
$css="flex: none;"
|
||||
/>
|
||||
<BoxButton
|
||||
onClick={() => {
|
||||
editor.focus();
|
||||
document.querySelector(`.bn-editor`)?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
|
||||
{t('Back to top')}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
<BoxButton
|
||||
onClick={() => {
|
||||
editor.focus();
|
||||
document
|
||||
.querySelector(
|
||||
`.bn-editor > .bn-block-group > .bn-block-outer:last-child`,
|
||||
)
|
||||
?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
|
||||
{t('Go to bottom')}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
</Box>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './TableContent';
|
||||
@@ -1 +0,0 @@
|
||||
export * from './useDocTableContentStore';
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface UseDocTableContentStore {
|
||||
isPanelTableContentOpen: boolean;
|
||||
setIsPanelTableContentOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const useDocTableContentStore = create<UseDocTableContentStore>(
|
||||
(set) => ({
|
||||
isPanelTableContentOpen: false,
|
||||
setIsPanelTableContentOpen: (isPanelTableContentOpen) => {
|
||||
set(() => ({ isPanelTableContentOpen }));
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -20,7 +20,6 @@
|
||||
"Content modal to delete document": "Contenu modal pour supprimer le document",
|
||||
"Content modal to export the document": "Contenu modal pour exporter le document",
|
||||
"Cookies placed": "Cookies déposés",
|
||||
"Copy link": "Copier le lien",
|
||||
"Copyright": "Copyright",
|
||||
"Create a new document": "Créer un nouveau document",
|
||||
"Create the document": "Créer le document",
|
||||
@@ -30,10 +29,9 @@
|
||||
"Defender of Rights - Free response - 71120 75342 Paris CEDEX 07": "Défenseur des droits - Réponse gratuite - 71120 75342 Paris CEDEX 07",
|
||||
"Delete document": "Supprimer le document",
|
||||
"Deleting the document \"{{title}}\"": "Suppression du document \"{{title}}\"",
|
||||
"Doc visibility card": "Carte de visibilité du doc",
|
||||
"Docs": "Docs",
|
||||
"Docs Description": "Description de Docs",
|
||||
"Docs Logo": "Logo Docs",
|
||||
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs : Votre nouveau compagnon pour collaborer sur des documents efficacement, intuitivement et en toute sécurité.",
|
||||
"Document icon": "Icône du document",
|
||||
"Document name": "Nom du document",
|
||||
"Document panel": "Panneau du document",
|
||||
@@ -47,7 +45,6 @@
|
||||
"Export": "Exporter",
|
||||
"Export your document, it will be inserted in the selected template.": "Exportez votre document, il sera inséré dans le modèle sélectionné.",
|
||||
"Failed to add the member in the document.": "Impossible d'ajouter le membre dans le document.",
|
||||
"Failed to copy link": "Échec de la copie du lien",
|
||||
"Failed to create the invitation for {{email}}.": "Impossible de créer l'invitation pour {{email}}.",
|
||||
"Find a member to add to the document": "Trouver un membre à ajouter au document",
|
||||
"French Interministerial Directorate for Digital Affairs (DINUM), 20 avenue de Ségur 75007 Paris.": "Direction interministérielle des affaires numériques (DINUM), 20 avenue de Segur 75007 Paris.",
|
||||
@@ -59,6 +56,7 @@
|
||||
"Invitation sent to {{email}}.": "Invitation envoyée à {{email}}.",
|
||||
"Invite new members to {{title}}": "Invitez de nouveaux membres à rejoindre {{title}}",
|
||||
"Invited": "Invité",
|
||||
"Is it public ?": "Est-ce public?",
|
||||
"It is the card information about the document.": "Il s'agit de la carte d'information du document.",
|
||||
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Il semble que la page que vous cherchez n'existe pas ou ne puisse pas être affichée correctement.",
|
||||
"It's true, you didn't have to click on a block that covers half the page to say you agree to the placement of cookies — even if you don't know what it means!": "C'est vrai, vous n'avez pas à cliquer sur un bloc qui couvre la moitié de la page pour dire que vous acceptez le placement de cookies — même si vous ne savez pas ce que cela signifie !",
|
||||
@@ -66,14 +64,13 @@
|
||||
"Language Icon": "Icône de langue",
|
||||
"Legal Notice": "Mentions Legales",
|
||||
"Legal notice": "Mention légale",
|
||||
"Link Copied !": "Lien copié !",
|
||||
"List invitation card": "Carte de liste d'invitation",
|
||||
"List members card": "Carte liste des membres",
|
||||
"Login": "Connexion",
|
||||
"Logout": "Se déconnecter",
|
||||
"Members": "Membres",
|
||||
"Modal confirmation to restore the version": "Modale de confirmation pour restaurer la version",
|
||||
"More info?": "Plus d'infos ?",
|
||||
"My account": "Mon compte",
|
||||
"No editor found": "Pas d'éditeur trouvé",
|
||||
"Nothing exceptional, no special privileges related to a .gouv.fr.": "Rien d'exceptionnel, pas de privilèges spéciaux liés à un .gouv.fr.",
|
||||
"Offline ?!": "Hors-ligne ?!",
|
||||
@@ -97,16 +94,16 @@
|
||||
"Restore this version": "Restaurer cette version",
|
||||
"Restore this version?": "Restaurer cette version?",
|
||||
"Role": "Rôle",
|
||||
"SUMMARY": "RÉSUMÉ",
|
||||
"Search by email": "Recherche par email",
|
||||
"Send a letter by post (free of charge, no stamp needed):": "Envoyer un courrier par la poste (gratuit, ne pas mettre de timbre):",
|
||||
"Share": "Partager",
|
||||
"Something bad happens, please retry.": "Une erreur inattendue s'est produite, veuillez réessayer.",
|
||||
"Stéphanie Schaer: Interministerial Digital Director (DINUM).": "Stéphanie Schaer: Directrice numérique interministériel (DINUM).",
|
||||
"Table of contents": "Table des matières",
|
||||
"Summary": "Résumé",
|
||||
"Template": "Template",
|
||||
"The document has been deleted.": "Le document a bien été supprimé.",
|
||||
"The document has been updated.": "Le document a été mis à jour.",
|
||||
"The document visiblitity has been updated.": "La visibilité du document a été mise à jour.",
|
||||
"The invitation has been removed.": "L'invitation a été supprimée.",
|
||||
"The member has been removed from the document": "Le membre a été retiré du document",
|
||||
"The role has been updated": "Le rôle a été mis à jour",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impress",
|
||||
"version": "1.4.0",
|
||||
"version": "1.3.0",
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"name": "eslint-config-impress",
|
||||
"version": "1.4.0",
|
||||
"version": "1.3.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "14.2.11",
|
||||
"@tanstack/eslint-plugin-query": "5.56.1",
|
||||
"@next/eslint-plugin-next": "14.2.9",
|
||||
"@tanstack/eslint-plugin-query": "5.53.0",
|
||||
"@typescript-eslint/eslint-plugin": "*",
|
||||
"@typescript-eslint/parser": "*",
|
||||
"eslint": "*",
|
||||
"eslint-config-next": "14.2.11",
|
||||
"eslint-config-next": "14.2.9",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-import": "2.30.0",
|
||||
"eslint-plugin-jest": "28.8.3",
|
||||
"eslint-plugin-jsx-a11y": "6.10.0",
|
||||
"eslint-plugin-playwright": "1.6.2",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"eslint-plugin-react": "7.36.1",
|
||||
"eslint-plugin-react": "7.35.2",
|
||||
"eslint-plugin-testing-library": "6.3.0",
|
||||
"prettier": "3.3.3"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "packages-i18n",
|
||||
"version": "1.4.0",
|
||||
"version": "1.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"extract-translation": "yarn extract-translation:impress",
|
||||
@@ -11,7 +11,7 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/jest": "29.5.13",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/node": "*",
|
||||
"eslint-config-impress": "*",
|
||||
"eslint-plugin-import": "2.30.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server-y-provider",
|
||||
"version": "1.4.0",
|
||||
"version": "1.3.0",
|
||||
"description": "Y.js provider for docs",
|
||||
"repository": "https://github.com/numerique-gouv/impress",
|
||||
"license": "MIT",
|
||||
|
||||
+13180
-12018
File diff suppressed because it is too large
Load Diff
@@ -72,8 +72,7 @@ frontend:
|
||||
envVars:
|
||||
PORT: 8080
|
||||
NEXT_PUBLIC_API_ORIGIN: https://impress.127.0.0.1.nip.io
|
||||
NEXT_PUBLIC_Y_PROVIDER_URL: wss://impress.127.0.0.1.nip.io/ws
|
||||
NEXT_PUBLIC_MEDIA_URL: https://impress.127.0.0.1.nip.io
|
||||
NEXT_PUBLIC_SIGNALING_URL: wss://impress.127.0.0.1.nip.io/ws
|
||||
|
||||
replicas: 1
|
||||
command:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
image:
|
||||
repository: lasuite/impress-backend
|
||||
pullPolicy: Always
|
||||
tag: "v1.4.0-preprod"
|
||||
tag: "v1.3.0-preprod"
|
||||
|
||||
backend:
|
||||
migrateJobAnnotations:
|
||||
@@ -124,13 +124,13 @@ frontend:
|
||||
image:
|
||||
repository: lasuite/impress-frontend
|
||||
pullPolicy: Always
|
||||
tag: "v1.4.0-preprod"
|
||||
tag: "v1.3.0-preprod"
|
||||
|
||||
yProvider:
|
||||
image:
|
||||
repository: lasuite/impress-y-provider
|
||||
pullPolicy: Always
|
||||
tag: "v1.4.0-preprod"
|
||||
tag: "v1.3.0-preprod"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
image:
|
||||
repository: lasuite/impress-backend
|
||||
pullPolicy: Always
|
||||
tag: "v1.4.0"
|
||||
tag: "v1.3.0"
|
||||
|
||||
backend:
|
||||
migrateJobAnnotations:
|
||||
@@ -124,13 +124,13 @@ frontend:
|
||||
image:
|
||||
repository: lasuite/impress-frontend
|
||||
pullPolicy: Always
|
||||
tag: "v1.4.0"
|
||||
tag: "v1.3.0"
|
||||
|
||||
yProvider:
|
||||
image:
|
||||
repository: lasuite/impress-y-provider
|
||||
pullPolicy: Always
|
||||
tag: "v1.4.0"
|
||||
tag: "v1.3.0"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.4.0",
|
||||
"version": "1.3.0",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
Reference in New Issue
Block a user