Compare commits
48 Commits
v2.0.0-pre
...
feature/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4676e994f | ||
|
|
01c7e548eb | ||
|
|
7b0e190d20 | ||
|
|
46c9210566 | ||
|
|
63fde1c60f | ||
|
|
8120b2f075 | ||
|
|
b38782a7d8 | ||
|
|
62a2e3f39f | ||
|
|
6d5b357b59 | ||
|
|
92f27341e0 | ||
|
|
d4e0cd57de | ||
|
|
6bafde99b1 | ||
|
|
ab55cad320 | ||
|
|
7a747b47db | ||
|
|
cdbd1ec7bb | ||
|
|
656ab8bc24 | ||
|
|
bc4d41a130 | ||
|
|
6e572256ce | ||
|
|
b14e976a18 | ||
|
|
efdbc3a14e | ||
|
|
5a18302bd4 | ||
|
|
2efdad980f | ||
|
|
35d37b0f10 | ||
|
|
c60561a6a7 | ||
|
|
9dd4513085 | ||
|
|
a64311b072 | ||
|
|
32ce99ca85 | ||
|
|
b9045a9678 | ||
|
|
e436fa801c | ||
|
|
ecfda62a8d | ||
|
|
bc6785a585 | ||
|
|
b34c00ff34 | ||
|
|
bcb15a143e | ||
|
|
7b70652440 | ||
|
|
0eac08dad9 | ||
|
|
298470229f | ||
|
|
827a090a92 | ||
|
|
35b0221ca9 | ||
|
|
2f20caf1df | ||
|
|
4608e101e6 | ||
|
|
29ca1b5dcf | ||
|
|
0da21cc3c5 | ||
|
|
88fd28783b | ||
|
|
248a8affce | ||
|
|
520854c3e2 | ||
|
|
ab0c1ad837 | ||
|
|
208a3279cf | ||
|
|
b06c9fbeda |
1
.github/workflows/docker-hub.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'main-new-ui'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
37
CHANGELOG.md
@@ -9,33 +9,29 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.0.0] - 2025-01-13
|
||||
|
||||
## Added
|
||||
|
||||
- 🔧(backend) add option to configure list of essential OIDC claims #525 & #531
|
||||
- 🔧(backend) add option to configure list of required OIDC claims #525
|
||||
- 🔧(helm) add option to disable default tls setting by @dominikkaminski #519
|
||||
- ✨(frontend) WIP: New ui
|
||||
- 💄(frontend) Add left panel #420
|
||||
- 💄(frontend) add filtering to left panel #475
|
||||
- ✨(frontend) new share modal ui #489
|
||||
- ✨(frontend) add favorite feature #515
|
||||
|
||||
## Changed
|
||||
|
||||
- 🏗️(yjs-server) organize yjs server #528
|
||||
- ♻️(frontend) better separation collaboration process #528
|
||||
- 💄(frontend) updating the header and leftpanel for responsive #421
|
||||
- 💄(frontend) update DocsGrid component #431
|
||||
- 💄(frontend) update DocsGridOptions component #432
|
||||
- 💄(frontend) update DocHeader ui #448
|
||||
- 💄(frontend) update DocHeader ui #446
|
||||
- 💄(frontend) update doc versioning ui #463
|
||||
- 💄(frontend) update doc summary ui #473
|
||||
- 🐛(frontend) fix doc grid button #478
|
||||
- ✨(backend) add server-to-server API endpoint to create documents #467
|
||||
- ✨(frontend) new share modal ui #489
|
||||
- ✨(frontend) add favorite feature #515
|
||||
- ✨(frontend) many ui fixes #524
|
||||
- 💄(frontend) fix the ux of the new ui #539
|
||||
- 💄(frontend) fix minor bugs #546
|
||||
|
||||
## Fixed
|
||||
|
||||
- 🐛(backend) fix create document via s2s if sub unknown but email found #543
|
||||
- 🐛(frontend) hide search and create doc button if not authenticated #555
|
||||
- 🐛(backend) race condition creation issue #556
|
||||
## Changed
|
||||
- 💄(frontend) add filtering from left panel #475
|
||||
- 🏗️(yjs-server) organize yjs server #528
|
||||
- ♻️(frontend) better separation collaboration process #528
|
||||
|
||||
## [1.10.0] - 2024-12-17
|
||||
|
||||
@@ -102,6 +98,8 @@ and this project adheres to
|
||||
- 🌐(backend) add German translation #259
|
||||
- 🌐(frontend) add German translation #255
|
||||
- ✨(frontend) add a broadcast store #387
|
||||
- ✨(backend) config endpoint #425
|
||||
- 💄(frontend) update DocsGrid component #431
|
||||
- ✨(backend) whitelist pod's IP address #443
|
||||
- ✨(backend) config endpoint #425
|
||||
- ✨(frontend) config endpoint #424
|
||||
@@ -353,8 +351,7 @@ and this project adheres to
|
||||
- 🚀 Impress, project to manage your documents easily and collaboratively.
|
||||
|
||||
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v2.0.0...main
|
||||
[v2.0.0]: https://github.com/numerique-gouv/impress/releases/v2.0.0
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v1.10.0...main
|
||||
[v1.10.0]: https://github.com/numerique-gouv/impress/releases/v1.10.0
|
||||
[v1.9.0]: https://github.com/numerique-gouv/impress/releases/v1.9.0
|
||||
[v1.8.2]: https://github.com/numerique-gouv/impress/releases/v1.8.2
|
||||
|
||||
@@ -7,7 +7,7 @@ UNSET_USER=0
|
||||
|
||||
TERRAFORM_DIRECTORY="./env.d/terraform"
|
||||
COMPOSE_FILE="${REPO_DIR}/docker-compose.yml"
|
||||
COMPOSE_PROJECT="docs"
|
||||
COMPOSE_PROJECT="impress"
|
||||
|
||||
|
||||
# _set_user: set (or unset) default user id used to run docker commands
|
||||
|
||||
@@ -201,7 +201,7 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
"abilities",
|
||||
"created_at",
|
||||
"creator",
|
||||
"is_favorite",
|
||||
"is_avorite",
|
||||
"link_role",
|
||||
"link_reach",
|
||||
"nb_accesses",
|
||||
@@ -264,17 +264,13 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
|
||||
"""Create the document and associate it with the user or send an invitation."""
|
||||
language = validated_data.get("language", settings.LANGUAGE_CODE)
|
||||
|
||||
# Get the user on its sub (unique identifier). Default on email if allowed in settings
|
||||
email = validated_data["email"]
|
||||
|
||||
# Get the user based on the sub (unique identifier)
|
||||
try:
|
||||
user = models.User.objects.get_user_by_sub_or_email(
|
||||
validated_data["sub"], email
|
||||
)
|
||||
except models.DuplicateEmailError as err:
|
||||
raise serializers.ValidationError({"email": [err.message]}) from err
|
||||
|
||||
if user:
|
||||
user = models.User.objects.get(sub=validated_data["sub"])
|
||||
except (models.User.DoesNotExist, KeyError):
|
||||
user = None
|
||||
email = validated_data["email"]
|
||||
else:
|
||||
email = user.email
|
||||
language = user.language or language
|
||||
|
||||
@@ -283,9 +279,7 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
|
||||
validated_data["content"]
|
||||
)
|
||||
except ConversionError as err:
|
||||
raise serializers.ValidationError(
|
||||
{"content": ["Could not convert content"]}
|
||||
) from err
|
||||
raise exceptions.APIException(detail="could not convert content") from err
|
||||
|
||||
document = models.Document.objects.create(
|
||||
title=validated_data["title"],
|
||||
@@ -308,11 +302,7 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
|
||||
role=models.RoleChoices.OWNER,
|
||||
)
|
||||
|
||||
self._send_email_notification(document, validated_data, email, language)
|
||||
return document
|
||||
|
||||
def _send_email_notification(self, document, validated_data, email, language):
|
||||
"""Notify the user about the newly created document."""
|
||||
# Notify the user about the newly created document
|
||||
subject = validated_data.get("subject") or _(
|
||||
"A new document was created on your behalf!"
|
||||
)
|
||||
@@ -323,6 +313,8 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
|
||||
}
|
||||
document.send_email(subject, [email], context, language)
|
||||
|
||||
return document
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""
|
||||
This serializer does not support updates.
|
||||
|
||||
@@ -676,7 +676,7 @@ class DocumentViewSet(
|
||||
|
||||
# Fetch the document and check if the user has access
|
||||
try:
|
||||
document = models.Document.objects.get(pk=pk)
|
||||
document, _created = models.Document.objects.get_or_create(pk=pk)
|
||||
except models.Document.DoesNotExist as exc:
|
||||
logger.debug("Document with ID '%s' does not exist", pk)
|
||||
raise drf.exceptions.PermissionDenied() from exc
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Authentication Backends for the Impress core app."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -11,9 +9,7 @@ from mozilla_django_oidc.auth import (
|
||||
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
|
||||
)
|
||||
|
||||
from core.models import DuplicateEmailError, User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from core.models import User
|
||||
|
||||
|
||||
class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
@@ -61,31 +57,24 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
_("Invalid response format or token verification failed")
|
||||
) from e
|
||||
|
||||
return userinfo
|
||||
|
||||
def verify_claims(self, claims):
|
||||
"""
|
||||
Verify the presence of essential claims and the "sub" (which is mandatory as defined
|
||||
by the OIDC specification) to decide if authentication should be allowed.
|
||||
"""
|
||||
essential_claims = settings.USER_OIDC_ESSENTIAL_CLAIMS
|
||||
missing_claims = [claim for claim in essential_claims if claim not in claims]
|
||||
|
||||
# Validate required claims
|
||||
missing_claims = [
|
||||
claim
|
||||
for claim in settings.USER_OIDC_REQUIRED_CLAIMS
|
||||
if claim not in userinfo
|
||||
]
|
||||
if missing_claims:
|
||||
logger.error("Missing essential claims: %s", missing_claims)
|
||||
return False
|
||||
raise SuspiciousOperation(
|
||||
_("Missing required claims in user info: %(claims)s")
|
||||
% {"claims": ", ".join(missing_claims)}
|
||||
)
|
||||
|
||||
return True
|
||||
return userinfo
|
||||
|
||||
def get_or_create_user(self, access_token, id_token, payload):
|
||||
"""Return a User based on userinfo. Create a new user if no match is found."""
|
||||
|
||||
user_info = self.get_userinfo(access_token, id_token, payload)
|
||||
|
||||
if not self.verify_claims(user_info):
|
||||
raise SuspiciousOperation("Claims verification failed.")
|
||||
|
||||
sub = user_info["sub"]
|
||||
email = user_info.get("email")
|
||||
|
||||
# Get user's full name from OIDC fields defined in settings
|
||||
@@ -98,10 +87,13 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
"short_name": short_name,
|
||||
}
|
||||
|
||||
try:
|
||||
user = User.objects.get_user_by_sub_or_email(sub, email)
|
||||
except DuplicateEmailError as err:
|
||||
raise SuspiciousOperation(err.message) from err
|
||||
sub = user_info.get("sub")
|
||||
if not sub:
|
||||
raise SuspiciousOperation(
|
||||
_("User info contained no recognizable user identification")
|
||||
)
|
||||
|
||||
user = self.get_existing_user(sub, email)
|
||||
|
||||
if user:
|
||||
if not user.is_active:
|
||||
@@ -120,6 +112,18 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
)
|
||||
return full_name or None
|
||||
|
||||
def get_existing_user(self, sub, email):
|
||||
"""Fetch existing user by sub or email."""
|
||||
try:
|
||||
return User.objects.get(sub=sub)
|
||||
except User.DoesNotExist:
|
||||
if email and settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION:
|
||||
try:
|
||||
return User.objects.get(email=email)
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
return None
|
||||
|
||||
def update_user_if_needed(self, user, claims):
|
||||
"""Update user claims if they have changed."""
|
||||
has_changed = any(
|
||||
@@ -127,4 +131,4 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
)
|
||||
if has_changed:
|
||||
updated_claims = {key: value for key, value in claims.items() if value}
|
||||
self.UserModel.objects.filter(id=user.id).update(**updated_claims)
|
||||
self.UserModel.objects.filter(sub=user.sub).update(**updated_claims)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Declare and configure the models for the impress core application
|
||||
"""
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import hashlib
|
||||
import smtplib
|
||||
@@ -90,16 +89,6 @@ class LinkReachChoices(models.TextChoices):
|
||||
PUBLIC = "public", _("Public") # Even anonymous users can access the document
|
||||
|
||||
|
||||
class DuplicateEmailError(Exception):
|
||||
"""Raised when an email is already associated with a pre-existing user."""
|
||||
|
||||
def __init__(self, message=None, email=None):
|
||||
"""Set message and email to describe the exception."""
|
||||
self.message = message
|
||||
self.email = email
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class BaseModel(models.Model):
|
||||
"""
|
||||
Serves as an abstract base model for other models, ensuring that records are validated
|
||||
@@ -137,35 +126,6 @@ class BaseModel(models.Model):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class UserManager(auth_models.UserManager):
|
||||
"""Custom manager for User model with additional methods."""
|
||||
|
||||
def get_user_by_sub_or_email(self, sub, email):
|
||||
"""Fetch existing user by sub or email."""
|
||||
try:
|
||||
return self.get(sub=sub)
|
||||
except self.model.DoesNotExist as err:
|
||||
if not email:
|
||||
return None
|
||||
|
||||
if settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION:
|
||||
try:
|
||||
return self.get(email=email)
|
||||
except self.model.DoesNotExist:
|
||||
pass
|
||||
elif (
|
||||
self.filter(email=email).exists()
|
||||
and not settings.OIDC_ALLOW_DUPLICATE_EMAILS
|
||||
):
|
||||
raise DuplicateEmailError(
|
||||
_(
|
||||
"We couldn't find a user with this sub but the email is already "
|
||||
"associated with a registered user."
|
||||
)
|
||||
) from err
|
||||
return None
|
||||
|
||||
|
||||
class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
"""User model to work with OIDC only authentication."""
|
||||
|
||||
@@ -232,7 +192,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
),
|
||||
)
|
||||
|
||||
objects = UserManager()
|
||||
objects = auth_models.UserManager()
|
||||
|
||||
USERNAME_FIELD = "admin_email"
|
||||
REQUIRED_FIELDS = []
|
||||
@@ -979,10 +939,7 @@ class Invitation(BaseModel):
|
||||
super().clean()
|
||||
|
||||
# Check if an identity already exists for the provided email
|
||||
if (
|
||||
User.objects.filter(email=self.email).exists()
|
||||
and not settings.OIDC_ALLOW_DUPLICATE_EMAILS
|
||||
):
|
||||
if User.objects.filter(email=self.email).exists():
|
||||
raise exceptions.ValidationError(
|
||||
{"email": _("This email is already associated to a registered user.")}
|
||||
)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"""Unit tests for the Authentication Backends."""
|
||||
|
||||
import random
|
||||
import re
|
||||
from logging import Logger
|
||||
from unittest import mock
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.test.utils import override_settings
|
||||
@@ -65,33 +62,7 @@ def test_authentication_getter_existing_user_via_email(
|
||||
assert user == db_user
|
||||
|
||||
|
||||
def test_authentication_getter_email_none(monkeypatch):
|
||||
"""
|
||||
If no user is found with the sub and no email is provided, a new user should be created.
|
||||
"""
|
||||
|
||||
klass = OIDCAuthenticationBackend()
|
||||
db_user = UserFactory(email=None)
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
user_info = {"sub": "123"}
|
||||
if random.choice([True, False]):
|
||||
user_info["email"] = None
|
||||
return user_info
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
|
||||
# Since the sub and email didn't match, it should create a new user
|
||||
assert models.User.objects.count() == 2
|
||||
assert user != db_user
|
||||
assert user.sub == "123"
|
||||
|
||||
|
||||
def test_authentication_getter_existing_user_no_fallback_to_email_allow_duplicate(
|
||||
def test_authentication_getter_existing_user_no_fallback_to_email(
|
||||
settings, monkeypatch
|
||||
):
|
||||
"""
|
||||
@@ -104,7 +75,6 @@ def test_authentication_getter_existing_user_no_fallback_to_email_allow_duplicat
|
||||
|
||||
# Set the setting to False
|
||||
settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = False
|
||||
settings.OIDC_ALLOW_DUPLICATE_EMAILS = True
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {"sub": "123", "email": db_user.email}
|
||||
@@ -121,39 +91,6 @@ def test_authentication_getter_existing_user_no_fallback_to_email_allow_duplicat
|
||||
assert user.sub == "123"
|
||||
|
||||
|
||||
def test_authentication_getter_existing_user_no_fallback_to_email_no_duplicate(
|
||||
settings, monkeypatch
|
||||
):
|
||||
"""
|
||||
When the "OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION" setting is set to False,
|
||||
the system should not match users by email, even if the email matches.
|
||||
"""
|
||||
|
||||
klass = OIDCAuthenticationBackend()
|
||||
db_user = UserFactory()
|
||||
|
||||
# Set the setting to False
|
||||
settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = False
|
||||
settings.OIDC_ALLOW_DUPLICATE_EMAILS = False
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {"sub": "123", "email": db_user.email}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
with pytest.raises(
|
||||
SuspiciousOperation,
|
||||
match=(
|
||||
"We couldn't find a user with this sub but the email is already associated "
|
||||
"with a registered user."
|
||||
),
|
||||
):
|
||||
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
|
||||
|
||||
# Since the sub doesn't match, it should not create a new user
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
def test_authentication_getter_existing_user_with_email(
|
||||
django_assert_num_queries, monkeypatch
|
||||
):
|
||||
@@ -191,12 +128,11 @@ def test_authentication_getter_existing_user_with_email(
|
||||
("Jack", "Duy", "jack.duy@example.com"),
|
||||
],
|
||||
)
|
||||
def test_authentication_getter_existing_user_change_fields_sub(
|
||||
def test_authentication_getter_existing_user_change_fields(
|
||||
first_name, last_name, email, django_assert_num_queries, monkeypatch
|
||||
):
|
||||
"""
|
||||
It should update the email or name fields on the user when they change
|
||||
and the user was identified by its "sub".
|
||||
It should update the email or name fields on the user when they change.
|
||||
"""
|
||||
klass = OIDCAuthenticationBackend()
|
||||
user = UserFactory(
|
||||
@@ -226,48 +162,6 @@ def test_authentication_getter_existing_user_change_fields_sub(
|
||||
assert user.short_name == first_name
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"first_name, last_name, email",
|
||||
[
|
||||
("Jack", "Doe", "john.doe@example.com"),
|
||||
("John", "Duy", "john.doe@example.com"),
|
||||
],
|
||||
)
|
||||
def test_authentication_getter_existing_user_change_fields_email(
|
||||
first_name, last_name, email, django_assert_num_queries, monkeypatch
|
||||
):
|
||||
"""
|
||||
It should update the name fields on the user when they change
|
||||
and the user was identified by its "email" as fallback.
|
||||
"""
|
||||
klass = OIDCAuthenticationBackend()
|
||||
user = UserFactory(
|
||||
full_name="John Doe", short_name="John", email="john.doe@example.com"
|
||||
)
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {
|
||||
"sub": "123",
|
||||
"email": user.email,
|
||||
"first_name": first_name,
|
||||
"last_name": last_name,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
# One and only one additional update query when a field has changed
|
||||
with django_assert_num_queries(3):
|
||||
authenticated_user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
|
||||
assert user == authenticated_user
|
||||
user.refresh_from_db()
|
||||
assert user.email == email
|
||||
assert user.full_name == f"{first_name:s} {last_name:s}"
|
||||
assert user.short_name == first_name
|
||||
|
||||
|
||||
def test_authentication_getter_new_user_no_email(monkeypatch):
|
||||
"""
|
||||
If no user matches the user's info sub, a user should be created.
|
||||
@@ -319,6 +213,29 @@ def test_authentication_getter_new_user_with_email(monkeypatch):
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
def test_authentication_getter_invalid_token(django_assert_num_queries, monkeypatch):
|
||||
"""The user's info doesn't contain a sub."""
|
||||
klass = OIDCAuthenticationBackend()
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {
|
||||
"test": "123",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
with (
|
||||
django_assert_num_queries(0),
|
||||
pytest.raises(
|
||||
SuspiciousOperation,
|
||||
match="User info contained no recognizable user identification",
|
||||
),
|
||||
):
|
||||
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
|
||||
|
||||
assert models.User.objects.exists() is False
|
||||
|
||||
|
||||
@override_settings(OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo")
|
||||
@responses.activate
|
||||
def test_authentication_get_userinfo_json_response():
|
||||
@@ -424,7 +341,7 @@ def test_authentication_getter_existing_disabled_user_via_email(
|
||||
django_assert_num_queries, monkeypatch
|
||||
):
|
||||
"""
|
||||
If an existing user does not match the sub but matches the email and is disabled,
|
||||
If an existing user does not matches the sub but matches the email and is disabled,
|
||||
an error should be raised and a user should not be created.
|
||||
"""
|
||||
|
||||
@@ -450,100 +367,85 @@ def test_authentication_getter_existing_disabled_user_via_email(
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
# Essential claims
|
||||
|
||||
|
||||
def test_authentication_verify_claims_default(django_assert_num_queries, monkeypatch):
|
||||
"""The sub claim should be mandatory by default."""
|
||||
klass = OIDCAuthenticationBackend()
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {
|
||||
"test": "123",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
with (
|
||||
django_assert_num_queries(0),
|
||||
pytest.raises(
|
||||
KeyError,
|
||||
match="sub",
|
||||
),
|
||||
):
|
||||
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
|
||||
|
||||
assert models.User.objects.exists() is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"essential_claims, missing_claims",
|
||||
[
|
||||
(["email", "sub"], ["email"]),
|
||||
(["Email", "sub"], ["Email"]), # Case sensitivity
|
||||
],
|
||||
)
|
||||
@override_settings(OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo")
|
||||
@mock.patch.object(Logger, "error")
|
||||
def test_authentication_verify_claims_essential_missing(
|
||||
mock_logger,
|
||||
essential_claims,
|
||||
missing_claims,
|
||||
django_assert_num_queries,
|
||||
monkeypatch,
|
||||
):
|
||||
"""Ensure SuspiciousOperation is raised if essential claims are missing."""
|
||||
|
||||
klass = OIDCAuthenticationBackend()
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {
|
||||
"sub": "123",
|
||||
"last_name": "Doe",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
with (
|
||||
django_assert_num_queries(0),
|
||||
pytest.raises(
|
||||
SuspiciousOperation,
|
||||
match="Claims verification failed",
|
||||
),
|
||||
override_settings(USER_OIDC_ESSENTIAL_CLAIMS=essential_claims),
|
||||
):
|
||||
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
|
||||
|
||||
assert models.User.objects.exists() is False
|
||||
mock_logger.assert_called_once_with("Missing essential claims: %s", missing_claims)
|
||||
# Required claims
|
||||
|
||||
|
||||
@override_settings(
|
||||
OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo",
|
||||
USER_OIDC_ESSENTIAL_CLAIMS=["email", "last_name"],
|
||||
USER_OIDC_REQUIRED_CLAIMS=["email", "sub", "address"],
|
||||
)
|
||||
def test_authentication_verify_claims_success(django_assert_num_queries, monkeypatch):
|
||||
"""Ensure user is authenticated when all essential claims are present."""
|
||||
@responses.activate
|
||||
def test_authentication_get_userinfo_required_claims_missing():
|
||||
"""Ensure SuspiciousOperation is raised if required claims are missing."""
|
||||
|
||||
klass = OIDCAuthenticationBackend()
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {
|
||||
"email": "john.doe@example.com",
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(r".*/userinfo"),
|
||||
json={
|
||||
"last_name": "Doe",
|
||||
"email": "john.doe@example.com",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
oidc_backend = OIDCAuthenticationBackend()
|
||||
|
||||
with pytest.raises(
|
||||
SuspiciousOperation, match="Missing required claims in user info: sub, address"
|
||||
):
|
||||
oidc_backend.get_userinfo("fake_access_token", None, None)
|
||||
|
||||
|
||||
@override_settings(
|
||||
OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo",
|
||||
USER_OIDC_REQUIRED_CLAIMS=["email", "Sub"],
|
||||
)
|
||||
@responses.activate
|
||||
def test_authentication_get_userinfo_required_claims_case_sensitivity():
|
||||
"""Ensure the system respects case sensitivity for required claims."""
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(r".*/userinfo"),
|
||||
json={
|
||||
"sub": "123",
|
||||
}
|
||||
"last_name": "Doe",
|
||||
"email": "john.doe@example.com",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
oidc_backend = OIDCAuthenticationBackend()
|
||||
|
||||
with django_assert_num_queries(6):
|
||||
user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
with pytest.raises(
|
||||
SuspiciousOperation, match="Missing required claims in user info: Sub"
|
||||
):
|
||||
oidc_backend.get_userinfo("fake_access_token", None, None)
|
||||
|
||||
assert models.User.objects.filter(id=user.id).exists()
|
||||
|
||||
assert user.sub == "123"
|
||||
assert user.full_name == "Doe"
|
||||
assert user.short_name is None
|
||||
assert user.email == "john.doe@example.com"
|
||||
@override_settings(
|
||||
OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo",
|
||||
USER_OIDC_REQUIRED_CLAIMS=["email", "sub"],
|
||||
)
|
||||
@responses.activate
|
||||
def test_authentication_get_userinfo_required_claims_success():
|
||||
"""Ensure user is authenticated when required claims are present."""
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(r".*/userinfo"),
|
||||
json={
|
||||
"sub": "123",
|
||||
"last_name": "Doe",
|
||||
"email": "john.doe@example.com",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
oidc_backend = OIDCAuthenticationBackend()
|
||||
result = oidc_backend.get_userinfo("fake_access_token", None, None)
|
||||
|
||||
assert result["sub"] == "123"
|
||||
assert result.get("first_name") is None
|
||||
assert result["last_name"] == "Doe"
|
||||
assert result["email"] == "john.doe@example.com"
|
||||
|
||||
@@ -13,7 +13,6 @@ import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
from core.api.serializers import ServerCreateDocumentSerializer
|
||||
from core.models import Document, Invitation, User
|
||||
from core.services.converter_services import ConversionError, YdocConverter
|
||||
|
||||
@@ -21,7 +20,7 @@ pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_convert_md():
|
||||
def mock_convert_markdown():
|
||||
"""Mock YdocConverter.convert_markdown to return a converted content."""
|
||||
with patch.object(
|
||||
YdocConverter,
|
||||
@@ -170,11 +169,8 @@ def test_api_documents_create_for_owner_invalid_sub():
|
||||
|
||||
|
||||
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
|
||||
def test_api_documents_create_for_owner_existing(mock_convert_md):
|
||||
"""
|
||||
It should be possible to create a document on behalf of a pre-existing user
|
||||
by passing their sub and email.
|
||||
"""
|
||||
def test_api_documents_create_for_owner_existing(mock_convert_markdown):
|
||||
"""It should be possible to create a document on behalf of a pre-existing user."""
|
||||
user = factories.UserFactory(language="en-us")
|
||||
|
||||
data = {
|
||||
@@ -193,7 +189,7 @@ def test_api_documents_create_for_owner_existing(mock_convert_md):
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
mock_convert_markdown.assert_called_once_with("Document content")
|
||||
|
||||
document = Document.objects.get()
|
||||
assert response.json() == {"id": str(document.id)}
|
||||
@@ -217,10 +213,10 @@ def test_api_documents_create_for_owner_existing(mock_convert_md):
|
||||
|
||||
|
||||
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
|
||||
def test_api_documents_create_for_owner_new_user(mock_convert_md):
|
||||
def test_api_documents_create_for_owner_new_user(mock_convert_markdown):
|
||||
"""
|
||||
It should be possible to create a document on behalf of new users by
|
||||
passing their unknown sub and email address.
|
||||
passing only their email address.
|
||||
"""
|
||||
data = {
|
||||
"title": "My Document",
|
||||
@@ -238,7 +234,7 @@ def test_api_documents_create_for_owner_new_user(mock_convert_md):
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
mock_convert_markdown.assert_called_once_with("Document content")
|
||||
|
||||
document = Document.objects.get()
|
||||
assert response.json() == {"id": str(document.id)}
|
||||
@@ -268,190 +264,8 @@ def test_api_documents_create_for_owner_new_user(mock_convert_md):
|
||||
assert document.creator == user
|
||||
|
||||
|
||||
@override_settings(
|
||||
SERVER_TO_SERVER_API_TOKENS=["DummyToken"],
|
||||
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION=True,
|
||||
)
|
||||
def test_api_documents_create_for_owner_existing_user_email_no_sub_with_fallback(
|
||||
mock_convert_md,
|
||||
):
|
||||
"""
|
||||
It should be possible to create a document on behalf of a pre-existing user for
|
||||
who the sub was not found if the settings allow it. This edge case should not
|
||||
happen in a healthy OIDC federation but can be usefull if an OIDC provider modifies
|
||||
users sub on each login for example...
|
||||
"""
|
||||
user = factories.UserFactory(language="en-us")
|
||||
|
||||
data = {
|
||||
"title": "My Document",
|
||||
"content": "Document content",
|
||||
"sub": "123",
|
||||
"email": user.email,
|
||||
}
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/create-for-owner/",
|
||||
data,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Bearer DummyToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
|
||||
document = Document.objects.get()
|
||||
assert response.json() == {"id": str(document.id)}
|
||||
|
||||
assert document.title == "My Document"
|
||||
assert document.content == "Converted document content"
|
||||
assert document.creator == user
|
||||
assert document.accesses.filter(user=user, role="owner").exists()
|
||||
|
||||
assert Invitation.objects.exists() is False
|
||||
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
assert email.to == [user.email]
|
||||
assert email.subject == "A new document was created on your behalf!"
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "A new document was created on your behalf!" in email_content
|
||||
assert (
|
||||
"You have been granted ownership of a new document: My Document"
|
||||
) in email_content
|
||||
|
||||
|
||||
@override_settings(
|
||||
SERVER_TO_SERVER_API_TOKENS=["DummyToken"],
|
||||
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION=False,
|
||||
OIDC_ALLOW_DUPLICATE_EMAILS=False,
|
||||
)
|
||||
def test_api_documents_create_for_owner_existing_user_email_no_sub_no_fallback(
|
||||
mock_convert_md,
|
||||
):
|
||||
"""
|
||||
When a user does not match an existing sub and fallback to matching on email is
|
||||
not allowed in settings, it should raise an error if the email is already used by
|
||||
a registered user and duplicate emails are not allowed.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
data = {
|
||||
"title": "My Document",
|
||||
"content": "Document content",
|
||||
"sub": "123",
|
||||
"email": user.email,
|
||||
}
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/create-for-owner/",
|
||||
data,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Bearer DummyToken",
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"email": [
|
||||
(
|
||||
"We couldn't find a user with this sub but the email is already "
|
||||
"associated with a registered user."
|
||||
)
|
||||
]
|
||||
}
|
||||
assert mock_convert_md.called is False
|
||||
assert Document.objects.exists() is False
|
||||
assert Invitation.objects.exists() is False
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
|
||||
@override_settings(
|
||||
SERVER_TO_SERVER_API_TOKENS=["DummyToken"],
|
||||
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION=False,
|
||||
OIDC_ALLOW_DUPLICATE_EMAILS=True,
|
||||
)
|
||||
def test_api_documents_create_for_owner_new_user_no_sub_no_fallback_allow_duplicate(
|
||||
mock_convert_md,
|
||||
):
|
||||
"""
|
||||
When a user does not match an existing sub and fallback to matching on email is
|
||||
not allowed in settings, it should be possible to create a new user with the same
|
||||
email as an existing user if the settings allow it (identification is still done
|
||||
via the sub in this case).
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
data = {
|
||||
"title": "My Document",
|
||||
"content": "Document content",
|
||||
"sub": "123",
|
||||
"email": user.email,
|
||||
}
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/create-for-owner/",
|
||||
data,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Bearer DummyToken",
|
||||
)
|
||||
assert response.status_code == 201
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
|
||||
document = Document.objects.get()
|
||||
assert response.json() == {"id": str(document.id)}
|
||||
|
||||
assert document.title == "My Document"
|
||||
assert document.content == "Converted document content"
|
||||
assert document.creator is None
|
||||
assert document.accesses.exists() is False
|
||||
|
||||
invitation = Invitation.objects.get()
|
||||
assert invitation.email == user.email
|
||||
assert invitation.role == "owner"
|
||||
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
assert email.to == [user.email]
|
||||
assert email.subject == "A new document was created on your behalf!"
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "A new document was created on your behalf!" in email_content
|
||||
assert (
|
||||
"You have been granted ownership of a new document: My Document"
|
||||
) in email_content
|
||||
|
||||
# The creator field on the document should be set when the user is created
|
||||
user = User.objects.create(email=user.email, password="!")
|
||||
document.refresh_from_db()
|
||||
assert document.creator == user
|
||||
|
||||
|
||||
@patch.object(ServerCreateDocumentSerializer, "_send_email_notification")
|
||||
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"], LANGUAGE_CODE="de-de")
|
||||
def test_api_documents_create_for_owner_with_default_language(
|
||||
mock_send, mock_convert_md
|
||||
):
|
||||
"""The default language from settings should apply by default."""
|
||||
data = {
|
||||
"title": "My Document",
|
||||
"content": "Document content",
|
||||
"sub": "123",
|
||||
"email": "john.doe@example.com",
|
||||
}
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/create-for-owner/",
|
||||
data,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Bearer DummyToken",
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
assert mock_send.call_args[0][3] == "de-de"
|
||||
|
||||
|
||||
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
|
||||
def test_api_documents_create_for_owner_with_custom_language(mock_convert_md):
|
||||
def test_api_documents_create_for_owner_with_custom_language(mock_convert_markdown):
|
||||
"""
|
||||
Test creating a document with a specific language.
|
||||
Useful if the remote server knows the user's language.
|
||||
@@ -473,7 +287,7 @@ def test_api_documents_create_for_owner_with_custom_language(mock_convert_md):
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
mock_convert_markdown.assert_called_once_with("Document content")
|
||||
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
@@ -488,7 +302,7 @@ def test_api_documents_create_for_owner_with_custom_language(mock_convert_md):
|
||||
|
||||
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
|
||||
def test_api_documents_create_for_owner_with_custom_subject_and_message(
|
||||
mock_convert_md,
|
||||
mock_convert_markdown,
|
||||
):
|
||||
"""It should be possible to customize the subject and message of the invitation email."""
|
||||
data = {
|
||||
@@ -509,7 +323,7 @@ def test_api_documents_create_for_owner_with_custom_subject_and_message(
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
mock_convert_markdown.assert_called_once_with("Document content")
|
||||
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
@@ -522,11 +336,11 @@ def test_api_documents_create_for_owner_with_custom_subject_and_message(
|
||||
|
||||
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
|
||||
def test_api_documents_create_for_owner_with_converter_exception(
|
||||
mock_convert_md,
|
||||
mock_convert_markdown,
|
||||
):
|
||||
"""In case of converter error, a 400 error should be raised."""
|
||||
"""It should be possible to customize the subject and message of the invitation email."""
|
||||
|
||||
mock_convert_md.side_effect = ConversionError("Conversion failed")
|
||||
mock_convert_markdown.side_effect = ConversionError("Conversion failed")
|
||||
|
||||
data = {
|
||||
"title": "My Document",
|
||||
@@ -543,33 +357,8 @@ def test_api_documents_create_for_owner_with_converter_exception(
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Bearer DummyToken",
|
||||
)
|
||||
mock_convert_md.assert_called_once_with("Document content")
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"content": ["Could not convert content"]}
|
||||
mock_convert_markdown.assert_called_once_with("Document content")
|
||||
|
||||
|
||||
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
|
||||
def test_api_documents_create_for_owner_with_empty_content():
|
||||
"""The content should not be empty or a 400 error should be raised."""
|
||||
|
||||
data = {
|
||||
"title": "My Document",
|
||||
"content": " ",
|
||||
"sub": "123",
|
||||
"email": "john.doe@example.com",
|
||||
}
|
||||
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/create-for-owner/",
|
||||
data,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Bearer DummyToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"content": [
|
||||
"This field may not be blank.",
|
||||
],
|
||||
}
|
||||
assert response.status_code == 500
|
||||
assert response.json() == {"detail": "could not convert content"}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
"""
|
||||
Unit tests for the User model
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from impress.settings import Base
|
||||
|
||||
|
||||
def test_invalid_settings_oidc_email_configuration():
|
||||
"""
|
||||
The OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION and OIDC_ALLOW_DUPLICATE_EMAILS settings
|
||||
should not be both set to True simultaneously.
|
||||
"""
|
||||
|
||||
class TestSettings(Base):
|
||||
"""Fake test settings."""
|
||||
|
||||
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = True
|
||||
OIDC_ALLOW_DUPLICATE_EMAILS = True
|
||||
|
||||
# The validation is performed during post_setup
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
TestSettings().post_setup()
|
||||
|
||||
# Check the exception message
|
||||
assert str(excinfo.value) == (
|
||||
"Both OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION and "
|
||||
"OIDC_ALLOW_DUPLICATE_EMAILS cannot be set to True simultaneously. "
|
||||
)
|
||||
@@ -474,17 +474,8 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# WARNING: Enabling this setting allows multiple user accounts to share the same email
|
||||
# address. This may cause security issues and is not recommended for production use when
|
||||
# email is activated as fallback for identification (see previous setting).
|
||||
OIDC_ALLOW_DUPLICATE_EMAILS = values.BooleanValue(
|
||||
default=False,
|
||||
environ_name="OIDC_ALLOW_DUPLICATE_EMAILS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
USER_OIDC_ESSENTIAL_CLAIMS = values.ListValue(
|
||||
default=[], environ_name="USER_OIDC_ESSENTIAL_CLAIMS", environ_prefix=None
|
||||
USER_OIDC_REQUIRED_CLAIMS = values.ListValue(
|
||||
default=[], environ_name="USER_OIDC_REQUIRED_CLAIMS", environ_prefix=None
|
||||
)
|
||||
USER_OIDC_FIELDS_TO_FULLNAME = values.ListValue(
|
||||
default=["first_name", "last_name"],
|
||||
@@ -634,15 +625,6 @@ class Base(Configuration):
|
||||
with sentry_sdk.configure_scope() as scope:
|
||||
scope.set_extra("application", "backend")
|
||||
|
||||
if (
|
||||
cls.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION
|
||||
and cls.OIDC_ALLOW_DUPLICATE_EMAILS
|
||||
):
|
||||
raise ValueError(
|
||||
"Both OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION and "
|
||||
"OIDC_ALLOW_DUPLICATE_EMAILS cannot be set to True simultaneously. "
|
||||
)
|
||||
|
||||
|
||||
class Build(Base):
|
||||
"""Settings used when the application is built.
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: lasuite-people\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-12-17 15:50+0000\n"
|
||||
"PO-Revision-Date: 2025-01-14 15:14\n"
|
||||
"PO-Revision-Date: 2024-12-17 15:53\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -31,23 +31,23 @@ msgstr "Wichtige Daten"
|
||||
|
||||
#: core/api/filters.py:16
|
||||
msgid "Creator is me"
|
||||
msgstr "Ersteller bin ich"
|
||||
msgstr ""
|
||||
|
||||
#: core/api/filters.py:19
|
||||
msgid "Favorite"
|
||||
msgstr "Favorit"
|
||||
msgstr ""
|
||||
|
||||
#: core/api/filters.py:22
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
msgstr ""
|
||||
|
||||
#: core/api/serializers.py:307
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
|
||||
msgstr ""
|
||||
|
||||
#: core/api/serializers.py:311
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Sie sind Besitzer eines neuen Dokuments:"
|
||||
msgstr ""
|
||||
|
||||
#: core/api/serializers.py:414
|
||||
msgid "Body"
|
||||
@@ -63,15 +63,15 @@ msgstr "Format"
|
||||
|
||||
#: core/authentication/backends.py:57
|
||||
msgid "Invalid response format or token verification failed"
|
||||
msgstr "Ungültiges Antwortformat oder Token-Verifizierung fehlgeschlagen"
|
||||
msgstr ""
|
||||
|
||||
#: core/authentication/backends.py:81
|
||||
msgid "User info contained no recognizable user identification"
|
||||
msgstr "Benutzerinfo enthielt keine erkennbare Benutzeridentifikation"
|
||||
msgstr ""
|
||||
|
||||
#: core/authentication/backends.py:88
|
||||
msgid "User account is disabled"
|
||||
msgstr "Benutzerkonto ist deaktiviert"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:62 core/models.py:69
|
||||
msgid "Reader"
|
||||
@@ -127,31 +127,31 @@ msgstr "Datum und Uhrzeit, an dem zuletzt aktualisiert wurde"
|
||||
|
||||
#: core/models.py:135
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
|
||||
msgstr "Geben Sie eine gültige Unterseite ein. Dieser Wert darf nur Buchstaben, Zahlen und die @/./+/-/_/: Zeichen enthalten."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:141
|
||||
msgid "sub"
|
||||
msgstr "unter"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:143
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
|
||||
msgstr "Erforderlich. 255 Zeichen oder weniger. Buchstaben, Zahlen und die Zeichen @/./+/-/_/:"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:152
|
||||
msgid "full name"
|
||||
msgstr "Name"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:153
|
||||
msgid "short name"
|
||||
msgstr "Kurzbezeichnung"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:155
|
||||
msgid "identity email address"
|
||||
msgstr "Identitäts-E-Mail-Adresse"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:160
|
||||
msgid "admin email address"
|
||||
msgstr "Admin E-Mail-Adresse"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:167
|
||||
msgid "language"
|
||||
@@ -159,35 +159,35 @@ msgstr "Sprache"
|
||||
|
||||
#: core/models.py:168
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "Die Sprache, in der der Benutzer die Benutzeroberfläche sehen möchte."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:174
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Die Zeitzone, in der der Nutzer Zeiten sehen möchte."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:177
|
||||
msgid "device"
|
||||
msgstr "Gerät"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:179
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Ob der Benutzer ein Gerät oder ein echter Benutzer ist."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:182
|
||||
msgid "staff status"
|
||||
msgstr "Status des Teammitgliedes"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:184
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Gibt an, ob der Benutzer sich in diese Admin-Seite einloggen kann."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:187
|
||||
msgid "active"
|
||||
msgstr "aktiviert"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:190
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Ob dieser Benutzer als aktiviert behandelt werden soll. Deaktivieren Sie diese Option, anstatt Konten zu löschen."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:202
|
||||
msgid "user"
|
||||
@@ -216,25 +216,25 @@ msgstr "Unbenanntes Dokument"
|
||||
#: core/models.py:593
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:597
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} hat Sie mit der Rolle \"{role}\" zu folgendem Dokument eingeladen:"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:600
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} hat ein Dokument mit Ihnen geteilt: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:623
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:624
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:630
|
||||
msgid "A link trace already exists for this document/user."
|
||||
@@ -242,23 +242,23 @@ msgstr ""
|
||||
|
||||
#: core/models.py:653
|
||||
msgid "Document favorite"
|
||||
msgstr "Dokumentenfavorit"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:654
|
||||
msgid "Document favorites"
|
||||
msgstr "Dokumentfavoriten"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:660
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Dieses Dokument ist bereits durch den gleichen Benutzer favorisiert worden."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:682
|
||||
msgid "Document/user relation"
|
||||
msgstr "Dokument/Benutzerbeziehung"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:683
|
||||
msgid "Document/user relations"
|
||||
msgstr "Dokument/Benutzerbeziehungen"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:689
|
||||
msgid "This user is already in this document."
|
||||
@@ -294,101 +294,101 @@ msgstr "Ob diese Vorlage für jedermann öffentlich ist."
|
||||
|
||||
#: core/models.py:731
|
||||
msgid "Template"
|
||||
msgstr "Vorlage"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:732
|
||||
msgid "Templates"
|
||||
msgstr "Vorlagen"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:871
|
||||
msgid "Template/user relation"
|
||||
msgstr "Vorlage/Benutzer-Beziehung"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:872
|
||||
msgid "Template/user relations"
|
||||
msgstr "Vorlage/Benutzerbeziehungen"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:878
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:884
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Dieses Team ist bereits in diesem Template."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:907
|
||||
msgid "email address"
|
||||
msgstr "E-Mail-Adresse"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:926
|
||||
msgid "Document invitation"
|
||||
msgstr "Einladung zum Dokument"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:927
|
||||
msgid "Document invitations"
|
||||
msgstr "Dokumenteinladungen"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:944
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:159 core/templates/mail/text/hello.txt:3
|
||||
msgid "Company logo"
|
||||
msgstr "Unternehmens-Logo"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
|
||||
#, python-format
|
||||
msgid "Hello %(name)s"
|
||||
msgstr "Guten Tag %(name)s!"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
|
||||
msgid "Hello"
|
||||
msgstr "Hallo"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:189 core/templates/mail/text/hello.txt:6
|
||||
msgid "Thank you very much for your visit!"
|
||||
msgstr "Vielen Dank für Ihren Besuch!"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:221
|
||||
#, python-format
|
||||
msgid "This mail has been sent to %(email)s by <a href=\"%(href)s\">%(name)s</a>"
|
||||
msgstr "Diese E-Mail wurde an %(email)s von <a href=\"%(href)s\">%(name)s</a> gesendet"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo-E-Mail"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Öffnen"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||
msgstr " Docs, Ihr neues unentbehrliches Werkzeug für die Organisation, den Austausch und die Zusammenarbeit in Ihren Dokumenten als Team. "
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr " Erstellt von %(brandname)s "
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/text/hello.txt:8
|
||||
#, python-format
|
||||
msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
|
||||
msgstr "Diese E-Mail wurde an %(email)s von %(name)s [%(href)s ] gesendet"
|
||||
msgstr ""
|
||||
|
||||
#: impress/settings.py:236
|
||||
msgid "English"
|
||||
msgstr "Englisch"
|
||||
msgstr ""
|
||||
|
||||
#: impress/settings.py:237
|
||||
msgid "French"
|
||||
msgstr "Französisch"
|
||||
msgstr ""
|
||||
|
||||
#: impress/settings.py:238
|
||||
msgid "German"
|
||||
msgstr "Deutsch"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "2.0.0"
|
||||
version = "1.10.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -204,15 +204,6 @@ test.describe('Doc Editor', () => {
|
||||
await verifyDocName(page, firstDoc);
|
||||
await expect(editor.getByText('Hello World Doc 2')).toBeHidden();
|
||||
await expect(editor.getByText('Hello World Doc 1')).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'New doc',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(editor.getByText('Hello World Doc 1')).toBeHidden();
|
||||
await expect(editor.getByText('Hello World Doc 2')).toBeHidden();
|
||||
});
|
||||
|
||||
test('it saves the doc when we change pages', async ({
|
||||
|
||||
@@ -160,7 +160,7 @@ test.describe('Document create member', () => {
|
||||
await page.getByRole('button', { name: 'Partager' }).click();
|
||||
|
||||
const inputSearch = page.getByRole('combobox', {
|
||||
name: 'Saisie de recherche rapide',
|
||||
name: 'Quick search input',
|
||||
});
|
||||
|
||||
const email = randomName('test@test.fr', browserName, 1)[0];
|
||||
|
||||
@@ -232,9 +232,6 @@ test.describe('Doc Visibility: Public', () => {
|
||||
cardContainer.getByText('Public document', { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'search' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible();
|
||||
|
||||
const urlDoc = page.url();
|
||||
|
||||
await page
|
||||
@@ -248,8 +245,6 @@ test.describe('Doc Visibility: Public', () => {
|
||||
await page.goto(urlDoc);
|
||||
|
||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'search' })).toBeHidden();
|
||||
await expect(page.getByRole('button', { name: 'New doc' })).toBeHidden();
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
|
||||
const card = page.getByLabel('It is the card information');
|
||||
await expect(card).toBeVisible();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "2.0.0",
|
||||
"version": "1.10.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
@@ -13,12 +13,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.49.1",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/node": "*",
|
||||
"@types/pdf-parse": "1.1.4",
|
||||
"eslint-config-impress": "*",
|
||||
"typescript": "*",
|
||||
"luxon": "3.5.0",
|
||||
"typescript": "*"
|
||||
"@types/luxon": "3.4.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"convert-stream": "1.0.2",
|
||||
|
||||
@@ -5,6 +5,7 @@ const config = {
|
||||
colors: {
|
||||
'card-border': '#ededed',
|
||||
'primary-bg': '#FAFAFA',
|
||||
'primary-action': '#1212FF',
|
||||
'primary-050': '#F5F5FE',
|
||||
'primary-100': '#EDF5FA',
|
||||
'primary-150': '#E5EEFA',
|
||||
@@ -59,6 +60,11 @@ const config = {
|
||||
h4: '1.375rem',
|
||||
h5: '1.25rem',
|
||||
h6: '1.125rem',
|
||||
'xl-alt': '5rem',
|
||||
'lg-alt': '4.5rem',
|
||||
'md-alt': '4rem',
|
||||
'sm-alt': '3.5rem',
|
||||
'xs-alt': '3rem',
|
||||
},
|
||||
weights: {
|
||||
thin: 100,
|
||||
@@ -224,7 +230,7 @@ const config = {
|
||||
'color-hover': 'var(--c--theme--colors--primary-700)',
|
||||
},
|
||||
border: {
|
||||
color: 'var(--c--theme--colors--primary-200)',
|
||||
color: 'var(--c--theme--colors--greyscale-300)',
|
||||
},
|
||||
},
|
||||
tertiary: {
|
||||
@@ -379,8 +385,8 @@ const config = {
|
||||
'color-active': '#EDEDED',
|
||||
},
|
||||
border: {
|
||||
color: 'var(--c--theme--colors--primary-600)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-600)',
|
||||
color: 'var(--c--theme--colors--greyscale-300)',
|
||||
'color-hover': 'var(--c--theme--colors--greyscale-300)',
|
||||
},
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "2.0.0",
|
||||
"version": "1.10.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 1.9 KiB |
BIN
src/frontend/apps/impress/src/assets/common/logo-doc-big.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M47.2515 62.7264C49.7721 62.0196 52.1861 60.7298 53.7701 58.4505C55.3337 56.2187 55.774 53.3875 55.774 50.6632V10.749C55.774 10.0423 55.7356 9.33431 55.6503 8.63477C56.9023 9.12719 57.8672 9.94087 58.5451 11.0758C59.3448 12.3663 59.7447 14.0657 59.7447 16.1741V56.7153C59.7447 59.5689 59.0449 61.7046 57.6454 63.1223C56.2458 64.54 54.1374 65.2489 51.3202 65.2489H36.0065C36.3692 65.1854 36.7328 65.12 37.0972 65.0528C40.2928 64.4817 43.6701 63.7067 47.2256 62.7336L47.2515 62.7264Z" fill="#C9191E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4521 55.8703V15.2746C10.4521 12.8027 11.1156 10.9306 12.4424 9.65832C13.7874 8.38601 15.5686 7.68624 17.7861 7.55901C21.0577 7.35908 24.1567 7.08644 27.083 6.7411C30.0093 6.37758 32.8084 5.95954 35.4803 5.48697C38.1703 5.01439 40.7876 4.48729 43.3322 3.90567C45.9495 3.28769 47.958 3.59668 49.3575 4.83264C50.757 6.06859 51.4568 8.04067 51.4568 10.7489V50.663C51.4568 53.044 51.0479 54.8161 50.2299 55.9794C49.412 57.1608 48.0307 58.0242 46.0859 58.5695C42.6324 59.5146 39.379 60.2598 36.3254 60.8051C33.2719 61.3685 30.2911 61.7957 27.3829 62.0865C24.4748 62.3773 21.494 62.6045 18.4404 62.7681C15.914 62.9135 13.951 62.3864 12.5515 61.1868C11.1519 60.0053 10.4521 58.2332 10.4521 55.8703ZM20.4387 22.6454C24.4424 22.3905 28.1112 21.9992 31.4447 21.4709C32.2535 21.339 33.0611 21.2017 33.8657 21.0592C34.7076 20.9101 35.3165 20.1764 35.3165 19.3233C35.3165 18.2185 34.3166 17.3865 33.2308 17.5717C32.6071 17.6781 31.9801 17.7815 31.3497 17.8819C28.0496 18.4078 24.4029 18.7979 20.4098 19.052C19.8118 19.0906 19.336 19.2769 19.0302 19.6482C18.741 19.9994 18.5993 20.4218 18.5993 20.9033C18.5993 21.3928 18.7644 21.8188 19.0941 22.1691L19.0987 22.1737C19.456 22.531 19.91 22.6857 20.4387 22.6454ZM20.4374 31.6973C24.4416 31.4424 28.1108 31.051 31.4447 30.5227C34.7931 29.9768 38.1061 29.3397 41.3818 28.6117C42.0465 28.464 42.5444 28.2314 42.7935 27.8701C43.0383 27.5228 43.1594 27.1247 43.1594 26.6834C43.1594 26.1857 42.9698 25.757 42.601 25.4087C42.2038 25.0336 41.6486 24.9462 40.9915 25.0815L40.9873 25.0825C37.8658 25.7902 34.6533 26.4074 31.3497 26.9337C28.0496 27.4596 24.4029 27.8497 20.4098 28.1038C19.8118 28.1424 19.336 28.3287 19.0302 28.7C18.7424 29.0495 18.5993 29.4621 18.5993 29.9278C18.5993 30.4329 18.7625 30.8691 19.0939 31.2211L19.1034 31.2301C19.4595 31.5653 19.9088 31.7177 20.432 31.6976L20.4374 31.6973ZM20.4383 40.7488C24.4422 40.4758 28.111 40.0753 31.4446 39.547C34.7933 39.0192 38.1057 38.3913 41.3818 37.6633C42.0481 37.5152 42.5464 37.2729 42.7951 36.892C43.0376 36.5471 43.1594 36.1594 43.1594 35.735C43.1594 35.2373 42.9698 34.8086 42.601 34.4602C42.2038 34.0852 41.6486 33.9978 40.9915 34.1331L40.9885 34.1338C37.8666 34.8235 34.6537 35.4316 31.3497 35.958C28.0493 36.4839 24.4025 36.8832 20.4092 37.1554C19.8115 37.1941 19.3359 37.3804 19.0302 37.7516C18.7424 38.1011 18.5993 38.5137 18.5993 38.9794C18.5993 39.4845 18.7625 39.9206 19.0939 40.2727L19.1034 40.2816C19.4595 40.6168 19.9088 40.7693 20.432 40.7492L20.4383 40.7488ZM31.4447 48.5358C28.1112 49.0641 24.4424 49.4555 20.4388 49.7103C19.91 49.7506 19.456 49.596 19.0987 49.2386L19.0941 49.2339C18.7645 48.8837 18.5993 48.4577 18.5993 47.9682C18.5993 47.4867 18.741 47.0644 19.0302 46.7132C19.336 46.3418 19.812 46.1555 20.41 46.117C24.4031 45.8629 28.0496 45.4727 31.3497 44.9469C31.9801 44.8464 32.6071 44.743 33.2308 44.6366C34.3166 44.4514 35.3165 45.2834 35.3165 46.3882C35.3165 47.2413 34.7076 47.975 33.8657 48.1241C33.0611 48.2666 32.2535 48.404 31.4447 48.5358Z" fill="#000091"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src/frontend/apps/impress/src/assets/common/logo-docs-sm.png
Normal file
|
After Width: | Height: | Size: 743 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M47.2515 62.7264C49.7721 62.0196 52.1861 60.7298 53.7701 58.4505C55.3337 56.2187 55.774 53.3875 55.774 50.6632V10.749C55.774 10.0423 55.7356 9.33431 55.6503 8.63477C56.9023 9.12719 57.8672 9.94087 58.5451 11.0758C59.3448 12.3663 59.7447 14.0657 59.7447 16.1741V56.7153C59.7447 59.5689 59.0449 61.7046 57.6454 63.1223C56.2458 64.54 54.1374 65.2489 51.3202 65.2489H36.0065C36.3692 65.1854 36.7328 65.12 37.0972 65.0528C40.2928 64.4817 43.6701 63.7067 47.2256 62.7336L47.2515 62.7264Z" fill="#C9191E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4521 55.8703V15.2746C10.4521 12.8027 11.1156 10.9306 12.4424 9.65832C13.7874 8.38601 15.5686 7.68624 17.7861 7.55901C21.0577 7.35908 24.1567 7.08644 27.083 6.7411C30.0093 6.37758 32.8084 5.95954 35.4803 5.48697C38.1703 5.01439 40.7876 4.48729 43.3322 3.90567C45.9495 3.28769 47.958 3.59668 49.3575 4.83264C50.757 6.06859 51.4568 8.04067 51.4568 10.7489V50.663C51.4568 53.044 51.0479 54.8161 50.2299 55.9794C49.412 57.1608 48.0307 58.0242 46.0859 58.5695C42.6324 59.5146 39.379 60.2598 36.3254 60.8051C33.2719 61.3685 30.2911 61.7957 27.3829 62.0865C24.4748 62.3773 21.494 62.6045 18.4404 62.7681C15.914 62.9135 13.951 62.3864 12.5515 61.1868C11.1519 60.0053 10.4521 58.2332 10.4521 55.8703ZM20.4387 22.6454C24.4424 22.3905 28.1112 21.9992 31.4447 21.4709C32.2535 21.339 33.0611 21.2017 33.8657 21.0592C34.7076 20.9101 35.3165 20.1764 35.3165 19.3233C35.3165 18.2185 34.3166 17.3865 33.2308 17.5717C32.6071 17.6781 31.9801 17.7815 31.3497 17.8819C28.0496 18.4078 24.4029 18.7979 20.4098 19.052C19.8118 19.0906 19.336 19.2769 19.0302 19.6482C18.741 19.9994 18.5993 20.4218 18.5993 20.9033C18.5993 21.3928 18.7644 21.8188 19.0941 22.1691L19.0987 22.1737C19.456 22.531 19.91 22.6857 20.4387 22.6454ZM20.4374 31.6973C24.4416 31.4424 28.1108 31.051 31.4447 30.5227C34.7931 29.9768 38.1061 29.3397 41.3818 28.6117C42.0465 28.464 42.5444 28.2314 42.7935 27.8701C43.0383 27.5228 43.1594 27.1247 43.1594 26.6834C43.1594 26.1857 42.9698 25.757 42.601 25.4087C42.2038 25.0336 41.6486 24.9462 40.9915 25.0815L40.9873 25.0825C37.8658 25.7902 34.6533 26.4074 31.3497 26.9337C28.0496 27.4596 24.4029 27.8497 20.4098 28.1038C19.8118 28.1424 19.336 28.3287 19.0302 28.7C18.7424 29.0495 18.5993 29.4621 18.5993 29.9278C18.5993 30.4329 18.7625 30.8691 19.0939 31.2211L19.1034 31.2301C19.4595 31.5653 19.9088 31.7177 20.432 31.6976L20.4374 31.6973ZM20.4383 40.7488C24.4422 40.4758 28.111 40.0753 31.4446 39.547C34.7933 39.0192 38.1057 38.3913 41.3818 37.6633C42.0481 37.5152 42.5464 37.2729 42.7951 36.892C43.0376 36.5471 43.1594 36.1594 43.1594 35.735C43.1594 35.2373 42.9698 34.8086 42.601 34.4602C42.2038 34.0852 41.6486 33.9978 40.9915 34.1331L40.9885 34.1338C37.8666 34.8235 34.6537 35.4316 31.3497 35.958C28.0493 36.4839 24.4025 36.8832 20.4092 37.1554C19.8115 37.1941 19.3359 37.3804 19.0302 37.7516C18.7424 38.1011 18.5993 38.5137 18.5993 38.9794C18.5993 39.4845 18.7625 39.9206 19.0939 40.2727L19.1034 40.2816C19.4595 40.6168 19.9088 40.7693 20.432 40.7492L20.4383 40.7488ZM31.4447 48.5358C28.1112 49.0641 24.4424 49.4555 20.4388 49.7103C19.91 49.7506 19.456 49.596 19.0987 49.2386L19.0941 49.2339C18.7645 48.8837 18.5993 48.4577 18.5993 47.9682C18.5993 47.4867 18.741 47.0644 19.0302 46.7132C19.336 46.3418 19.812 46.1555 20.41 46.117C24.4031 45.8629 28.0496 45.4727 31.3497 44.9469C31.9801 44.8464 32.6071 44.743 33.2308 44.6366C34.3166 44.4514 35.3165 45.2834 35.3165 46.3882C35.3165 47.2413 34.7076 47.975 33.8657 48.1241C33.0611 48.2666 32.2535 48.404 31.4447 48.5358Z" fill="#000091"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/frontend/apps/impress/src/assets/common/pro-connect.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
29
src/frontend/apps/impress/src/components/DocsTitle.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import IconDocs from '@/assets/common/logo-docs-sm.png';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import Title from '@/features/header/components/Title/Title';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { Box } from './Box';
|
||||
|
||||
export const DocsTitle = () => {
|
||||
const theme = useCunninghamTheme();
|
||||
const { t } = useTranslation();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const spacings = theme.spacingsTokens();
|
||||
const colors = theme.colorsTokens();
|
||||
return (
|
||||
<Box
|
||||
$align="center"
|
||||
$gap={spacings['3xs']}
|
||||
$direction="row"
|
||||
$position="relative"
|
||||
$height="fit-content"
|
||||
>
|
||||
<Image priority src={IconDocs} alt={t('Docs Logo')} width={25} />
|
||||
<Title />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -6,8 +6,7 @@ export * from './DropdownMenu';
|
||||
export * from './Icon';
|
||||
export * from './InfiniteScroll';
|
||||
export * from './Link';
|
||||
export * from './LoadMoreText';
|
||||
export * from './SideModal';
|
||||
export * from './separators';
|
||||
export * from './separators/SeparatedSection';
|
||||
export * from './Text';
|
||||
export * from './TextErrors';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Command } from 'cmdk';
|
||||
import { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { HorizontalSeparator } from '@/components';
|
||||
import { HorizontalSeparator } from '@/components/separators/HorizontalSeparator';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { Box } from '../Box';
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './HorizontalSeparator';
|
||||
export * from './SeparatedSection';
|
||||
@@ -14,7 +14,7 @@ import { useAuthStore } from './useAuthStore';
|
||||
* 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];
|
||||
const regexpUrlsAuth = [/\/docs\/$/g];
|
||||
|
||||
export const Auth = ({ children }: PropsWithChildren) => {
|
||||
const { initAuth, initiated, authenticated, login, getAuthUrl } =
|
||||
|
||||
@@ -38,9 +38,13 @@ export const useAuthStore = create<AuthStore>((set, get) => ({
|
||||
});
|
||||
},
|
||||
login: () => {
|
||||
get().setAuthUrl(window.location.pathname);
|
||||
console.log(window.location.pathname);
|
||||
if (window.location.pathname !== '/') {
|
||||
get().setAuthUrl(window.location.pathname);
|
||||
window.location.replace(`/`);
|
||||
}
|
||||
|
||||
window.location.replace(`${baseApiUrl()}authenticate/`);
|
||||
// window.location.replace(`${baseApiUrl()}authenticate/`);
|
||||
},
|
||||
logout: () => {
|
||||
terminateCrispSession();
|
||||
|
||||
@@ -75,6 +75,7 @@ export const tokens = {
|
||||
'danger-text': '#fff',
|
||||
'card-border': '#ededed',
|
||||
'primary-bg': '#FAFAFA',
|
||||
'primary-action': '#1212FF',
|
||||
'primary-050': '#F5F5FE',
|
||||
'primary-150': '#E5EEFA',
|
||||
'primary-950': '#1B1B35',
|
||||
@@ -129,6 +130,11 @@ export const tokens = {
|
||||
ml: '0.938rem',
|
||||
xl: '1.25rem',
|
||||
t: '0.6875rem',
|
||||
'xl-alt': '5rem',
|
||||
'lg-alt': '4.5rem',
|
||||
'md-alt': '4rem',
|
||||
'sm-alt': '3.5rem',
|
||||
'xs-alt': '3rem',
|
||||
},
|
||||
weights: {
|
||||
thin: 100,
|
||||
@@ -315,7 +321,7 @@ export const tokens = {
|
||||
color: 'white',
|
||||
'color-hover': 'var(--c--theme--colors--primary-700)',
|
||||
},
|
||||
border: { color: 'var(--c--theme--colors--primary-200)' },
|
||||
border: { color: 'var(--c--theme--colors--greyscale-300)' },
|
||||
},
|
||||
tertiary: {
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
@@ -502,8 +508,8 @@ export const tokens = {
|
||||
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--greyscale-300)',
|
||||
'color-hover': 'var(--c--theme--colors--greyscale-300)',
|
||||
},
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
},
|
||||
|
||||
@@ -2,7 +2,8 @@ import { DateTime } from 'luxon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, HorizontalSeparator, Icon, Text } from '@/components';
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { HorizontalSeparator } from '@/components/separators/HorizontalSeparator';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import {
|
||||
Doc,
|
||||
|
||||
@@ -20,13 +20,14 @@ import { useAuthStore } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useEditorStore } from '@/features/docs/doc-editor/';
|
||||
import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management';
|
||||
import { DocShareModal } from '@/features/docs/doc-share';
|
||||
import {
|
||||
KEY_LIST_DOC_VERSIONS,
|
||||
ModalSelectVersion,
|
||||
} from '@/features/docs/doc-versioning';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocShareModal } from '../../doc-share/component/DocShareModal';
|
||||
|
||||
import { ModalPDF } from './ModalExport';
|
||||
|
||||
interface DocToolBoxProps {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, HorizontalSeparator } from '@/components';
|
||||
import { Box } from '@/components';
|
||||
import { HorizontalSeparator } from '@/components/separators/HorizontalSeparator';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { DocTitleText } from './DocTitle';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './useCreateDoc';
|
||||
export * from './useDeleteFavoriteDoc';
|
||||
export * from './useDoc';
|
||||
export * from './useDocOptions';
|
||||
export * from './useDocs';
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<svg width="32" height="36" viewBox="0 0 32 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2.01394" y="1.23611" width="25.9722" height="33.5278" rx="3.54167" fill="white"/>
|
||||
<rect x="2.01394" y="1.23611" width="25.9722" height="33.5278" rx="3.54167" stroke="#E3E3FD" stroke-width="0.472222"/>
|
||||
<path d="M6.5 8.55554H15" stroke="#6A6AF4" stroke-width="1.88889" stroke-linecap="round"/>
|
||||
<path d="M6.5 11.3889H23.5M6.5 14.2222H23.5M6.5 17.0556H23.5M6.5 19.8889H23.5M6.5 22.7222H20.6667" stroke="#CACAFB" stroke-width="1.88889" stroke-linecap="round"/>
|
||||
<rect x="7" y="10" width="16" height="16" rx="8" fill="#6A6AF4"/>
|
||||
<rect x="7" y="10" width="16" height="16" rx="8" stroke="white" stroke-width="1.5"/>
|
||||
<path d="M16.8 18L18 19.2V20.1H15.45V22.95L15 23.4L14.55 22.95V20.1H12V19.2L13.2 18V14.7H12.6V13.8H17.4V14.7H16.8V18Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 853 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="32" height="36" viewBox="0 0 32 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2.01394" y="1.23611" width="25.9722" height="33.5278" rx="3.54167" fill="white"/>
|
||||
<rect x="2.01394" y="1.23611" width="25.9722" height="33.5278" rx="3.54167" stroke="#E3E3FD" stroke-width="0.472222"/>
|
||||
<path d="M6.5 8.55554H15" stroke="#6A6AF4" stroke-width="1.88889" stroke-linecap="round"/>
|
||||
<path d="M6.5 11.3889H23.5M6.5 14.2222H23.5M6.5 17.0556H23.5M6.5 19.8889H23.5M6.5 22.7222H20.6667" stroke="#CACAFB" stroke-width="1.88889" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 568 B |
@@ -27,14 +27,9 @@ export const useCollaboration = (room?: string, initialContent?: Base64) => {
|
||||
setBroadcastProvider,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Destroy the provider when the component is unmounted
|
||||
*/
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (room) {
|
||||
destroyProvider();
|
||||
}
|
||||
destroyProvider();
|
||||
};
|
||||
}, [destroyProvider, room]);
|
||||
}, [destroyProvider]);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Icon } from '@/components';
|
||||
import { QuickSearchItemContent } from '@/components/quick-search/';
|
||||
import { QuickSearchItemContent } from '@/components/quick-search/QuickSearchItemContent';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import { SimpleDocItem } from '@/features/docs/docs-grid/';
|
||||
import { SimpleDocItem } from '@/features/docs/docs-grid/components/SimpleDocItem';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
type DocSearchItemProps = {
|
||||
|
||||
@@ -12,10 +12,10 @@ import {
|
||||
QuickSearchData,
|
||||
QuickSearchGroup,
|
||||
} from '@/components/quick-search';
|
||||
import { Doc, useInfiniteDocs } from '@/features/docs/doc-management';
|
||||
import EmptySearchIcon from '@/features/docs/doc-search/assets/illustration-docs-empty.png';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import EmptySearchIcon from '../assets/illustration-docs-empty.png';
|
||||
import { Doc, useInfiniteDocs } from '../../doc-management';
|
||||
|
||||
import { DocSearchItem } from './DocSearchItem';
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './DocSearchModal';
|
||||
@@ -1 +0,0 @@
|
||||
export * from './components';
|
||||
@@ -1,7 +1,9 @@
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { DropdownMenu, DropdownMenuOption, Text } from '@/components';
|
||||
import { Role, useTrans } from '@/features/docs/doc-management/';
|
||||
|
||||
import { useTrans } from '../../doc-management/hooks';
|
||||
import { Role } from '../../doc-management/types';
|
||||
|
||||
type Props = {
|
||||
currentRole: Role;
|
||||
@@ -12,11 +12,13 @@ import { Box } from '@/components';
|
||||
import { User } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, Role } from '@/features/docs';
|
||||
import {
|
||||
useCreateDocAccess,
|
||||
useCreateDocInvitation,
|
||||
} from '@/features/docs/doc-share';
|
||||
import { OptionType } from '@/features/docs/doc-share/types';
|
||||
import { useLanguage } from '@/i18n/hooks/useLanguage';
|
||||
|
||||
import { useCreateDocAccess, useCreateDocInvitation } from '../api';
|
||||
import { OptionType } from '../types';
|
||||
|
||||
import { DocRoleDropdown } from './DocRoleDropdown';
|
||||
import { DocShareAddMemberListItem } from './DocShareAddMemberListItem';
|
||||
|
||||
@@ -10,12 +10,14 @@ import {
|
||||
import { User } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, Role } from '@/features/docs/doc-management';
|
||||
|
||||
import { useDeleteDocInvitation, useUpdateDocInvitation } from '../api';
|
||||
import { Invitation } from '../types';
|
||||
import {
|
||||
useDeleteDocInvitation,
|
||||
useUpdateDocInvitation,
|
||||
} from '@/features/docs/doc-share';
|
||||
import { SearchUserRow } from '@/features/docs/doc-share/component/SearchUserRow';
|
||||
import { Invitation } from '@/features/docs/doc-share/types';
|
||||
|
||||
import { DocRoleDropdown } from './DocRoleDropdown';
|
||||
import { SearchUserRow } from './SearchUserRow';
|
||||
|
||||
type Props = {
|
||||
doc: Doc;
|
||||
@@ -8,14 +8,14 @@ import {
|
||||
IconOptions,
|
||||
} from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Access, Doc, Role } from '@/features/docs/doc-management/';
|
||||
import { SearchUserRow } from '@/features/docs/doc-share/component/SearchUserRow';
|
||||
import { useWhoAmI } from '@/features/docs/doc-share/hooks/useWhoAmI';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useDeleteDocAccess, useUpdateDocAccess } from '../api';
|
||||
import { useWhoAmI } from '../hooks/';
|
||||
import { Access, Doc, Role } from '../../doc-management/types';
|
||||
import { useDeleteDocAccess, useUpdateDocAccess } from '../index';
|
||||
|
||||
import { DocRoleDropdown } from './DocRoleDropdown';
|
||||
import { SearchUserRow } from './SearchUserRow';
|
||||
|
||||
type Props = {
|
||||
doc: Doc;
|
||||
@@ -4,24 +4,24 @@ import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle, css } from 'styled-components';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { Box, LoadMoreText } from '@/components';
|
||||
import { Box } from '@/components';
|
||||
import { LoadMoreText } from '@/components/LoadMoreText';
|
||||
import {
|
||||
QuickSearch,
|
||||
QuickSearchData,
|
||||
QuickSearchGroup,
|
||||
} from '@/components/quick-search/';
|
||||
} from '@/components/quick-search/QuickSearch';
|
||||
import { QuickSearchGroup } from '@/components/quick-search/QuickSearchGroup';
|
||||
import { User } from '@/core';
|
||||
import { Access, Doc } from '@/features/docs';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
import { isValidEmail } from '@/utils';
|
||||
|
||||
import {
|
||||
KEY_LIST_USER,
|
||||
useDocAccessesInfinite,
|
||||
useDocInvitationsInfinite,
|
||||
useUsers,
|
||||
} from '../api';
|
||||
import { Invitation } from '../types';
|
||||
} from '@/features/docs/doc-share';
|
||||
import { Invitation } from '@/features/docs/doc-share/types';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
import { isValidEmail } from '@/utils';
|
||||
|
||||
import { DocShareAddMemberList } from './DocShareAddMemberList';
|
||||
import { DocShareInvitationItem } from './DocShareInvitationItem';
|
||||
@@ -94,7 +94,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
||||
count === 1
|
||||
? t('Document owner')
|
||||
: t('Share with {{count}} users', {
|
||||
count,
|
||||
count: count,
|
||||
}),
|
||||
elements: members,
|
||||
endActions: membersQuery.hasNextPage
|
||||
@@ -137,7 +137,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
||||
};
|
||||
|
||||
return {
|
||||
groupName: t('Search user result'),
|
||||
groupName: t('Search user result', { count: users.length }),
|
||||
elements: users,
|
||||
endActions:
|
||||
isEmail && users.length === 0
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, HorizontalSeparator } from '@/components';
|
||||
import { Box } from '@/components';
|
||||
import { HorizontalSeparator } from '@/components/separators/HorizontalSeparator';
|
||||
import { Doc } from '@/features/docs';
|
||||
|
||||
import { DocVisibility } from './DocVisibility';
|
||||
@@ -3,8 +3,7 @@ import { css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { User } from '@/core';
|
||||
|
||||
import { SearchUserRow } from './SearchUserRow';
|
||||
import { SearchUserRow } from '@/features/docs/doc-share/component/SearchUserRow';
|
||||
|
||||
type Props = {
|
||||
user: User;
|
||||
@@ -19,10 +19,9 @@ import {
|
||||
LinkRole,
|
||||
useUpdateDocLink,
|
||||
} from '@/features/docs';
|
||||
import { useTranslatedShareSettings } from '@/features/docs/doc-share';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useTranslatedShareSettings } from '../hooks/';
|
||||
|
||||
interface DocVisibilityProps {
|
||||
doc: Doc;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Box, Text } from '@/components';
|
||||
import {
|
||||
QuickSearchItemContent,
|
||||
QuickSearchItemContentProps,
|
||||
} from '@/components/quick-search';
|
||||
} from '@/components/quick-search/QuickSearchItemContent';
|
||||
import { User } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './DocShareModal';
|
||||
@@ -29,10 +29,10 @@ export const useTranslatedShareSettings = () => {
|
||||
icon: 'corporate_fare',
|
||||
value: LinkReach.AUTHENTICATED,
|
||||
descriptionReadOnly: t(
|
||||
'Anyone with the link can view the document if they are logged in',
|
||||
'Anyone with the link can see the document provided they are logged in',
|
||||
),
|
||||
descriptionEdit: t(
|
||||
'Anyone with the link can edit the document if they are logged in',
|
||||
'Anyone with the link can edit provided they are logged in',
|
||||
),
|
||||
},
|
||||
[LinkReach.PUBLIC]: {
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
export * from './api';
|
||||
export * from './components';
|
||||
export * from './hooks';
|
||||
export * from './types';
|
||||
|
||||
@@ -4,9 +4,9 @@ import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle, css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { DocEditor } from '@/features/docs/doc-editor';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
|
||||
import { DocEditor } from '../../doc-editor/components/DocEditor';
|
||||
import { Doc } from '../../doc-management';
|
||||
import { Versions } from '../types';
|
||||
|
||||
import { ModalConfirmationVersion } from './ModalConfirmationVersion';
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<svg
|
||||
width="32"
|
||||
height="36"
|
||||
viewBox="0 0 32 36"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="2.01394"
|
||||
y="1.23611"
|
||||
width="25.9722"
|
||||
height="33.5278"
|
||||
rx="3.54167"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="2.01394"
|
||||
y="1.23611"
|
||||
width="25.9722"
|
||||
height="33.5278"
|
||||
rx="3.54167"
|
||||
stroke="#E3E3FD"
|
||||
stroke-width="0.472222"
|
||||
/>
|
||||
<path
|
||||
d="M6.5 8.55554H15"
|
||||
stroke="#6A6AF4"
|
||||
stroke-width="1.88889"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M6.5 11.3889H23.5M6.5 14.2222H23.5M6.5 17.0556H23.5M6.5 19.8889H23.5M6.5 22.7222H20.6667"
|
||||
stroke="#CACAFB"
|
||||
stroke-width="1.88889"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<rect x="7" y="10" width="16" height="16" rx="8" fill="#6A6AF4" />
|
||||
<rect
|
||||
x="7"
|
||||
y="10"
|
||||
width="16"
|
||||
height="16"
|
||||
rx="8"
|
||||
stroke="white"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M16.8 18L18 19.2V20.1H15.45V22.95L15 23.4L14.55 22.95V20.1H12V19.2L13.2 18V14.7H12.6V13.8H17.4V14.7H16.8V18Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1017 B |
@@ -1,37 +0,0 @@
|
||||
<svg
|
||||
width="32"
|
||||
height="36"
|
||||
viewBox="0 0 32 36"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="2.01394"
|
||||
y="1.23611"
|
||||
width="25.9722"
|
||||
height="33.5278"
|
||||
rx="3.54167"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="2.01394"
|
||||
y="1.23611"
|
||||
width="25.9722"
|
||||
height="33.5278"
|
||||
rx="3.54167"
|
||||
stroke="#E3E3FD"
|
||||
stroke-width="0.472222"
|
||||
/>
|
||||
<path
|
||||
d="M6.5 8.55554H15"
|
||||
stroke="#6A6AF4"
|
||||
stroke-width="1.88889"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M6.5 11.3889H23.5M6.5 14.2222H23.5M6.5 17.0556H23.5M6.5 19.8889H23.5M6.5 22.7222H20.6667"
|
||||
stroke="#CACAFB"
|
||||
stroke-width="1.88889"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 683 B |
@@ -4,12 +4,10 @@ import { InView } from 'react-intersection-observer';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Card, Text } from '@/components';
|
||||
import {
|
||||
DocDefaultFilter,
|
||||
useInfiniteDocs,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocDefaultFilter, useInfiniteDocs } from '../../doc-management';
|
||||
|
||||
import { DocsGridItem } from './DocsGridItem';
|
||||
import { DocsGridLoader } from './DocsGridLoader';
|
||||
|
||||
@@ -89,7 +87,7 @@ export const DocsGrid = ({
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{!hasDocs && !loading && (
|
||||
{!hasDocs && (
|
||||
<Box $padding={{ vertical: 'sm' }} $align="center" $justify="center">
|
||||
<Text $size="sm" $variation="600" $weight="700">
|
||||
{t('No documents found')}
|
||||
|
||||
@@ -7,9 +7,10 @@ import {
|
||||
KEY_LIST_DOC,
|
||||
ModalRemoveDoc,
|
||||
useCreateFavoriteDoc,
|
||||
useDeleteFavoriteDoc,
|
||||
} from '@/features/docs/doc-management';
|
||||
|
||||
import { useDeleteFavoriteDoc } from '../../doc-management/api/useDeleteFavoriteDoc';
|
||||
|
||||
interface DocsGridActionsProps {
|
||||
doc: Doc;
|
||||
openShareModal?: () => void;
|
||||
|
||||
@@ -3,10 +3,11 @@ import { DateTime } from 'luxon';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, StyledLink, Text } from '@/components';
|
||||
import { Doc, LinkReach } from '@/features/docs/doc-management';
|
||||
import { DocShareModal } from '@/features/docs/doc-share';
|
||||
import { DocShareModal } from '@/features/docs/doc-share/component/DocShareModal';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { Doc, LinkReach } from '../../doc-management';
|
||||
|
||||
import { DocsGridActions } from './DocsGridActions';
|
||||
import { SimpleDocItem } from './SimpleDocItem';
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@ import { css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, LinkReach } from '@/features/docs/doc-management';
|
||||
import { Doc, LinkReach } from '@/features/docs';
|
||||
import PinnedDocumentIcon from '@/features/docs/doc-management/assets/pinned-document.svg';
|
||||
import SimpleFileIcon from '@/features/docs/doc-management/assets/simple-document.svg';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import PinnedDocumentIcon from '../assets/pinned-document.svg';
|
||||
import SimpleFileIcon from '../assets/simple-document.svg';
|
||||
|
||||
const ItemTextCss = css`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -60,14 +60,13 @@ export const Header = () => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<StyledLink href="/">
|
||||
<StyledLink href="/docs">
|
||||
<Box
|
||||
$align="center"
|
||||
$gap={spacings['3xs']}
|
||||
$direction="row"
|
||||
$position="relative"
|
||||
$height="fit-content"
|
||||
$margin={{ top: 'auto' }}
|
||||
>
|
||||
<Image priority src={IconDocs} alt={t('Docs Logo')} width={25} />
|
||||
<Title />
|
||||
|
||||
@@ -1,26 +1,47 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Text } from '@/components/';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
const Title = () => {
|
||||
type TitleProps = {
|
||||
size?: 'sm' | 'md';
|
||||
};
|
||||
|
||||
const Title = ({ size = 'sm' }: TitleProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useCunninghamTheme();
|
||||
const spacings = theme.spacingsTokens();
|
||||
const colors = theme.colorsTokens();
|
||||
|
||||
return (
|
||||
<Box $direction="row" $align="center" $gap={spacings['2xs']}>
|
||||
<Text $margin="none" as="h2" $color="#000091" $zIndex={1} $size="1.30rem">
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap={size === 'sm' ? spacings['2xs'] : '18px'}
|
||||
>
|
||||
<Text
|
||||
$margin="none"
|
||||
as="h2"
|
||||
$color="#000091"
|
||||
$zIndex={1}
|
||||
$size={size === 'sm' ? '1.30rem' : '48px'}
|
||||
>
|
||||
{t('Docs')}
|
||||
</Text>
|
||||
<Text
|
||||
$padding={{ horizontal: 'xs', vertical: '1px' }}
|
||||
$size="11px"
|
||||
$padding={{
|
||||
horizontal: size === 'sm' ? '2xs' : '13px',
|
||||
vertical: size === 'sm' ? '3xs' : '9px',
|
||||
}}
|
||||
$size={size === 'sm' ? '11px' : '24px'}
|
||||
$theme="primary"
|
||||
$variation="500"
|
||||
$weight="bold"
|
||||
$radius="12px"
|
||||
$radius={size === 'sm' ? '12px' : '24px'}
|
||||
$css={css`
|
||||
line-height: ${size === 'sm' ? '16px' : '20px'};
|
||||
`}
|
||||
$background={colors['primary-200']}
|
||||
>
|
||||
BETA
|
||||
|
||||
|
After Width: | Height: | Size: 4.1 MiB |
BIN
src/frontend/apps/impress/src/features/home/assets/SC1.png
Normal file
|
After Width: | Height: | Size: 385 KiB |
BIN
src/frontend/apps/impress/src/features/home/assets/SC2.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
src/frontend/apps/impress/src/features/home/assets/SC3.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 26 KiB |
BIN
src/frontend/apps/impress/src/features/home/assets/SC4.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
src/frontend/apps/impress/src/features/home/assets/first.png
Normal file
|
After Width: | Height: | Size: 284 KiB |
BIN
src/frontend/apps/impress/src/features/home/assets/logo-docs.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/frontend/apps/impress/src/features/home/assets/logo.png
Normal file
|
After Width: | Height: | Size: 919 B |
@@ -0,0 +1,84 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { ProConnectButton } from '@/components/ProConnectButton';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import firstImage from '../assets/first.png';
|
||||
import DocLogo from '../assets/logo-docs.png';
|
||||
|
||||
export default function HomeBanner() {
|
||||
const { t } = useTranslation();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const spacings = spacingsTokens();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
|
||||
return (
|
||||
<Box $maxWidth="78rem" $width="100%" $justify="center" $align="center">
|
||||
<Box
|
||||
$width="100%"
|
||||
$padding={{ top: 'xxxl', bottom: 'calc(3.5rem + 94px)' }}
|
||||
$justify="space-between"
|
||||
$align="center"
|
||||
$height="calc(100dvh - 94px)"
|
||||
$position="relative"
|
||||
$direction={isDesktop ? 'row' : 'column'}
|
||||
>
|
||||
<Box
|
||||
$width="100%"
|
||||
$justify="center"
|
||||
$align="center"
|
||||
$padding={{ horizontal: '10px' }}
|
||||
$gap={spacings['sm']}
|
||||
>
|
||||
<Image src={DocLogo} alt="DocLogo" />
|
||||
<Text
|
||||
$size={isDesktop ? 'xs-alt' : '2.3rem'}
|
||||
$variation="800"
|
||||
$weight="bold"
|
||||
$textAlign="center"
|
||||
$css={css`
|
||||
line-height: 56px;
|
||||
`}
|
||||
>
|
||||
{t('Collaborative writing made simple')}
|
||||
</Text>
|
||||
<Text $size="lg" $variation="700" $textAlign="center">
|
||||
{t(
|
||||
'Collaborate and write in real time, without layout constraints.',
|
||||
)}
|
||||
</Text>
|
||||
<ProConnectButton />
|
||||
</Box>
|
||||
{isDesktop && (
|
||||
<Image src={firstImage} alt="first" style={{ maxWidth: '50%' }} />
|
||||
)}
|
||||
<Box
|
||||
$position="absolute"
|
||||
$padding="base"
|
||||
$justify="center"
|
||||
$align="center"
|
||||
$css={css`
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
`}
|
||||
>
|
||||
<Button
|
||||
href="#docs-app-info"
|
||||
color="secondary"
|
||||
icon={
|
||||
<Icon $theme="primary" $variation="800" iconName="expand_more" />
|
||||
}
|
||||
>
|
||||
Voir plus
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import IconDocs from '@/assets/common/logo-docs.svg';
|
||||
import { Box, Text } from '@/components';
|
||||
import { ProConnectButton } from '@/components/ProConnectButton';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Footer } from '@/features/footer';
|
||||
import Title from '@/features/header/components/Title/Title';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import SC1Responsive from '../assets/SC1-responsive.png';
|
||||
import SC1 from '../assets/SC1.png';
|
||||
import SC2 from '../assets/SC2.png';
|
||||
import SC3 from '../assets/SC3.png';
|
||||
import SC4Responsive from '../assets/SC4-responsive.png';
|
||||
import SC4 from '../assets/SC4.png';
|
||||
|
||||
import HomeBanner from './HomeBanner';
|
||||
import { HomeHeader } from './HomeHeader';
|
||||
import { HomeSection } from './HomeSection';
|
||||
|
||||
export default function HomeContent() {
|
||||
const { t } = useTranslation();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const spacings = spacingsTokens();
|
||||
return (
|
||||
<Box $background="white">
|
||||
<HomeHeader />
|
||||
<Box $align="center" $justify="center">
|
||||
<Box $maxWidth="78rem" $width="100%" $justify="center" $align="center">
|
||||
<HomeBanner />
|
||||
<Box
|
||||
id="docs-app-info"
|
||||
$gap={isDesktop ? '230px' : '115px'}
|
||||
$padding={{
|
||||
vertical: spacings['6xl'],
|
||||
}}
|
||||
>
|
||||
<HomeSection
|
||||
shadow={isDesktop}
|
||||
isColumn={true}
|
||||
illustration={isDesktop ? SC1 : SC1Responsive}
|
||||
title={t('An uncompromising writing experience.')}
|
||||
tag={t('Write')}
|
||||
description={t(
|
||||
'Docs offers an intuitive writing experience. Its minimalist interface favors content over layout, while offering the essentials: media import, offline mode and keyboard shortcuts for greater efficiency.',
|
||||
)}
|
||||
/>
|
||||
<HomeSection
|
||||
isColumn={false}
|
||||
illustration={SC2}
|
||||
title={t('Simple and secure collaboration.')}
|
||||
tag={t('Collaborate')}
|
||||
description={t(
|
||||
'Docs makes real-time collaboration simple. Invite collaborators - public officials or external partners - with one click to see their changes live, while maintaining precise access control for data security.',
|
||||
)}
|
||||
/>
|
||||
<HomeSection
|
||||
isColumn={false}
|
||||
reverse={true}
|
||||
illustration={SC3}
|
||||
title={t('Flexible export.')}
|
||||
tag={t('Export')}
|
||||
description={t(
|
||||
'To facilitate the circulation of documents, Docs allows you to export your content to the most common formats: PDF, Word or OpenDocument.',
|
||||
)}
|
||||
/>
|
||||
|
||||
<HomeSection
|
||||
illustration={isDesktop ? SC4 : SC4Responsive}
|
||||
title={t('A new way to organize knowledge.')}
|
||||
tag={t('Organize')}
|
||||
availableSoon={true}
|
||||
description={t(
|
||||
'Docs transforms your documents into knowledge bases thanks to subpages, powerful search and the ability to pin your important documents.',
|
||||
)}
|
||||
/>
|
||||
<Box
|
||||
$gap={spacings['md']}
|
||||
$justify="center"
|
||||
$align="center"
|
||||
$padding={{ vertical: '140px' }}
|
||||
>
|
||||
<Box
|
||||
$align="center"
|
||||
$gap={spacings['3xs']}
|
||||
$direction="row"
|
||||
$position="relative"
|
||||
$height="fit-content"
|
||||
>
|
||||
<IconDocs />
|
||||
<Title size="md" />
|
||||
</Box>
|
||||
<Text $size="md" $variation="1000" $textAlign="center">
|
||||
{t('Docs is already available, log in to use it now.')}
|
||||
</Text>
|
||||
<ProConnectButton />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Footer />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import IconDocs from '@/assets/common/logo-docs-sm.png';
|
||||
import { Box } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { LaGaufre } from '@/features/header/components/LaGaufre';
|
||||
import Title from '@/features/header/components/Title/Title';
|
||||
import { LanguagePicker } from '@/features/language';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
export const HomeHeader = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { themeTokens, spacingsTokens } = useCunninghamTheme();
|
||||
const logo = themeTokens().logo;
|
||||
const spacings = spacingsTokens();
|
||||
return (
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$align="center"
|
||||
$width="100%"
|
||||
$padding={{ horizontal: '18px', vertical: 'base' }}
|
||||
>
|
||||
<Box $align="center" $gap="3rem" $direction="row">
|
||||
{logo && (
|
||||
<Image
|
||||
priority
|
||||
src={logo.src}
|
||||
alt={logo.alt}
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: 109, height: 'auto' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isDesktop && (
|
||||
<Box
|
||||
$align="center"
|
||||
$gap={spacings['3xs']}
|
||||
$direction="row"
|
||||
$position="relative"
|
||||
$height="fit-content"
|
||||
>
|
||||
<Image src={IconDocs} alt="Docs app logo" />
|
||||
<Title />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box $direction="row" $gap="1rem" $align="center">
|
||||
<LanguagePicker />
|
||||
<LaGaufre />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
import Image, { ImageProps } from 'next/image';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
export type HomeSectionProps = {
|
||||
illustration?: ImageProps['src'];
|
||||
title: string;
|
||||
description: string;
|
||||
tag: string;
|
||||
availableSoon?: boolean;
|
||||
shadow?: boolean;
|
||||
isColumn?: boolean;
|
||||
reverse?: boolean;
|
||||
};
|
||||
export const HomeSection = ({
|
||||
illustration,
|
||||
title,
|
||||
description,
|
||||
tag,
|
||||
reverse = false,
|
||||
isColumn = true,
|
||||
availableSoon = false,
|
||||
shadow = false,
|
||||
}: HomeSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const spacings = spacingsTokens();
|
||||
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
|
||||
const direction = useMemo(() => {
|
||||
if (!isDesktop) {
|
||||
return 'column';
|
||||
} else if (isColumn) {
|
||||
return reverse ? 'column-reverse' : 'column';
|
||||
}
|
||||
|
||||
return reverse ? 'row-reverse' : 'row';
|
||||
}, [isColumn, isDesktop, reverse]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
$direction={direction}
|
||||
$gap={spacings['lg']}
|
||||
$padding={{
|
||||
horizontal: isDesktop ? '6xl' : spacings['md'],
|
||||
}}
|
||||
$align={isDesktop ? 'flex-start' : 'center'}
|
||||
$justify={isDesktop ? 'flex-start' : 'center'}
|
||||
>
|
||||
<Box $gap={spacings['sm']} $maxWidth="850px" $width="100%">
|
||||
<Box $direction="row" $gap={spacings['sm']} $wrap="wrap">
|
||||
<SectionTag tag={tag} />
|
||||
{availableSoon && (
|
||||
<SectionTag tag={t('Available soon')} availableSoon />
|
||||
)}
|
||||
</Box>
|
||||
<Text
|
||||
$css={css`
|
||||
line-height: 50px;
|
||||
`}
|
||||
$variation="1000"
|
||||
$weight="bold"
|
||||
$size={isDesktop ? 'xs-alt' : 'h1'}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<Text $variation="700" $weight="400" $size="md">
|
||||
{description}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{illustration && (
|
||||
<Image
|
||||
src={illustration}
|
||||
alt="SC4Illustration"
|
||||
style={{
|
||||
maxWidth: 'calc(100dvw - 50px)',
|
||||
height: 'auto',
|
||||
boxShadow: shadow
|
||||
? '0px 5px 25.1px 0px rgba(0, 0, 0, 0.08)'
|
||||
: 'none',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const SectionTag = ({
|
||||
tag,
|
||||
availableSoon,
|
||||
}: {
|
||||
tag: string;
|
||||
availableSoon?: boolean;
|
||||
}) => {
|
||||
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
|
||||
const spacings = spacingsTokens();
|
||||
const colors = colorsTokens();
|
||||
return (
|
||||
<Box
|
||||
$background={
|
||||
!availableSoon ? colors['primary-100'] : colors['warning-100']
|
||||
}
|
||||
$padding={{ horizontal: spacings['sm'], vertical: '6px' }}
|
||||
$css={css`
|
||||
align-self: flex-start;
|
||||
border-radius: 4px;
|
||||
`}
|
||||
>
|
||||
<Text
|
||||
$size="md"
|
||||
$variation={availableSoon ? '600' : '800'}
|
||||
$weight="bold"
|
||||
$theme={availableSoon ? 'warning' : 'primary'}
|
||||
>
|
||||
{tag}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import Image from 'next/image';
|
||||
|
||||
import SC1 from '../assets/SC1.png';
|
||||
import SC1Responsive from '../assets/SC1-responsive.png';
|
||||
|
||||
import { HomeSection } from './HomeSection';
|
||||
|
||||
export const HomeWriteSection = () => {
|
||||
return (
|
||||
<HomeSection
|
||||
isColumn={true}
|
||||
illustration={<Image src={illustration} alt="DocLogo" />}
|
||||
title="Une expérience d'écriture sans compromis."
|
||||
tag="Écrire"
|
||||
description="Docs propose une expérience d'écriture intuitive. Son interface minimaliste privilégie le contenu sur la mise en page, tout en offrant l'essentiel : import de médias, mode hors-ligne et raccourcis clavier pour plus d'efficacité."
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -2,8 +2,8 @@ import { css } from 'styled-components';
|
||||
|
||||
import { Box, SeparatedSection } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useDocStore } from '@/features/docs/doc-management';
|
||||
import { SimpleDocItem } from '@/features/docs/docs-grid';
|
||||
import { useDocStore } from '@/features/docs';
|
||||
import { SimpleDocItem } from '@/features/docs/docs-grid/components/SimpleDocItem';
|
||||
|
||||
export const LeftPanelDocContent = () => {
|
||||
const { currentDoc } = useDocStore();
|
||||
|
||||
@@ -3,9 +3,8 @@ import { css } from 'styled-components';
|
||||
|
||||
import { Box, StyledLink } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import { DocShareModal } from '@/features/docs/doc-share';
|
||||
import { DocsGridActions, SimpleDocItem } from '@/features/docs/docs-grid';
|
||||
import { Doc, DocsGridActions, SimpleDocItem } from '@/features/docs';
|
||||
import { DocShareModal } from '@/features/docs/doc-share/component/DocShareModal';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
type LeftPanelFavoriteItemProps = {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, HorizontalSeparator, InfiniteScroll, Text } from '@/components';
|
||||
import { Box, InfiniteScroll, Text } from '@/components';
|
||||
import { HorizontalSeparator } from '@/components/separators/HorizontalSeparator';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useInfiniteDocs } from '@/features/docs/doc-management';
|
||||
import { useInfiniteDocs } from '@/features/docs';
|
||||
|
||||
import { LeftPanelFavoriteItem } from './LeftPanelFavoriteItem';
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@ import { useRouter } from 'next/navigation';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { Box, Icon, SeparatedSection } from '@/components';
|
||||
import { useAuthStore } from '@/core';
|
||||
import { useCreateDoc } from '@/features/docs/doc-management';
|
||||
import { DocSearchModal } from '@/features/docs/doc-search';
|
||||
import { useCreateDoc } from '@/features/docs';
|
||||
import { DocSearchModal } from '@/features/docs/doc-search/components/DocSearchModal';
|
||||
import { useCmdK } from '@/hook/useCmdK';
|
||||
|
||||
import { useLeftPanelStore } from '../stores';
|
||||
@@ -14,7 +13,6 @@ import { useLeftPanelStore } from '../stores';
|
||||
export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
|
||||
const router = useRouter();
|
||||
const searchModal = useModal();
|
||||
const auth = useAuthStore();
|
||||
useCmdK(searchModal.open);
|
||||
const { togglePanel } = useLeftPanelStore();
|
||||
|
||||
@@ -54,20 +52,16 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
|
||||
<Icon $variation="800" $theme="primary" iconName="house" />
|
||||
}
|
||||
/>
|
||||
{auth.authenticated && (
|
||||
<Button
|
||||
onClick={searchModal.open}
|
||||
size="medium"
|
||||
color="tertiary-text"
|
||||
icon={
|
||||
<Icon $variation="800" $theme="primary" iconName="search" />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
onClick={searchModal.open}
|
||||
size="medium"
|
||||
color="tertiary-text"
|
||||
icon={
|
||||
<Icon $variation="800" $theme="primary" iconName="search" />
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
{auth.authenticated && (
|
||||
<Button onClick={createNewDoc}>{t('New doc')}</Button>
|
||||
)}
|
||||
<Button onClick={createNewDoc}>{t('New doc')}</Button>
|
||||
</Box>
|
||||
</SeparatedSection>
|
||||
{children}
|
||||
|
||||
@@ -2,145 +2,85 @@
|
||||
"de": {
|
||||
"translation": {
|
||||
"\"{{email}}\" is already invited to the document.": "\"{{email}}\" ist bereits zum Dokument eingeladen.",
|
||||
"\"{{email}}\" is already member of the document.": "\"{{email}}\" ist bereits Mitglied des Dokuments.",
|
||||
"AI Actions": "KI-Aktionen",
|
||||
"AI seems busy! Please try again.": "KI scheint beschäftigt! Bitte versuchen Sie es erneut.",
|
||||
"Accessibility": "Barrierefreiheit",
|
||||
"Accessibility statement": "Erklärung zur Barrierefreiheit",
|
||||
"Add": "Hinzufügen",
|
||||
"Address:": "Anschrift:",
|
||||
"Administrator": "Administrator",
|
||||
"All docs": "Alle Dokumente",
|
||||
"Anonymous": "Gast",
|
||||
"Anyone with the link can edit the document": "Jeder mit dem Link kann das Dokument bearbeiten",
|
||||
"Anyone with the link can edit the document if they are logged in": "Jeder mit dem Link kann das Dokument bearbeiten, wenn er angemeldet ist",
|
||||
"Anyone with the link can see the document": "Jeder mit dem Link kann das Dokument ansehen",
|
||||
"Anyone with the link can view the document if they are logged in": "Jeder mit dem Link kann das Dokument ansehen, wenn er angemeldet ist",
|
||||
"Anyone on the internet with the link can view": "Für jeden im Internet mit diesem Link sichtbar",
|
||||
"Are you sure you want to delete the document \"{{title}}\"?": "Sind Sie sicher, dass Sie das Dokument \"{{title}}\" löschen möchten?",
|
||||
"Back to home page": "Zurück zur Startseite",
|
||||
"Can't load this page, please check your internet connection.": "Diese Seite kann nicht geladen werden. Bitte überprüfen Sie Ihre Internetverbindung.",
|
||||
"Cancel": "Abbrechen",
|
||||
"Choose a role": "Wählen Sie eine Rolle",
|
||||
"Close the modal": "Pop up schliessen",
|
||||
"Compliance status": "Konformitätsstatus",
|
||||
"Confirm deletion": "Löschung bestätigen",
|
||||
"Connected": "Verbunden",
|
||||
"Content modal to delete document": "Inhalts-Modal zum Löschen des Dokuments",
|
||||
"Content modal to export the document": "Inhalte zum Exportieren des Dokuments",
|
||||
"Convert Markdown": "Markdown konvertieren",
|
||||
"Copied to clipboard": "In die Zwischenablage kopiert",
|
||||
"Copy as {{format}}": "Als {{format}} kopieren",
|
||||
"Copy link": "Link kopieren",
|
||||
"Correct": "Korrigieren",
|
||||
"Delete": "Löschen",
|
||||
"Delete a doc": "Dokument löschen",
|
||||
"Delete document": "Dokument löschen",
|
||||
"Deleting the document \"{{title}}\"": "Lösche das Dokument \"{{title}}\"",
|
||||
"Doc visibility card": "Dokumenten-Sichtbarkeitskarte",
|
||||
"Docs": "Docs",
|
||||
"Docs Logo": "Docs Logo",
|
||||
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Pages: Ihr neuer Begleiter für eine effiziente, intuitive und sichere Zusammenarbeit bei Dokumenten.",
|
||||
"Document owner": "Besitzer des Dokuments",
|
||||
"Document title updated successfully": "Titel des Dokuments erfolgreich aktualisiert",
|
||||
"Download": "Herunterladen",
|
||||
"E-mail:": "E-Mail:",
|
||||
"Edition": "Bearbeiten",
|
||||
"Editor": "Editor",
|
||||
"Editor unavailable": "Editor nicht verfügbar",
|
||||
"Error during delete invitation": "Fehler beim Löschen der Einladung",
|
||||
"Error during invitation update": "Fehler beim Aktualisieren der Einladung",
|
||||
"Error during update invitation": "Fehler beim Aktualisieren der Einladung",
|
||||
"Error while deleting invitation": "Fehler beim Löschen der Einladung",
|
||||
"Export": "Exportieren",
|
||||
"Failed to add the member in the document.": "Fehler beim Hinzufügen des Mitglieds zum Dokument.",
|
||||
"Failed to copy link": "Link konnte nicht kopiert werden",
|
||||
"Failed to copy to clipboard": "Fehler beim Kopieren in die Zwischenablage",
|
||||
"Failed to create the invitation for {{email}}.": "Fehler beim Erstellen der Einladung für {{email}}.",
|
||||
"Format": "Format",
|
||||
"History": "Versionsverlauf",
|
||||
"Find a member to add to the document": "Suchen Sie ein Mitglied, das dem Dokument hinzugefügt werden soll",
|
||||
"If a member is editing, his works can be lost.": "Wenn ein Mitglied editiert, können seine Änderungen verloren gehen.",
|
||||
"Improvement and contact": "Verbesserungen und Kontakt",
|
||||
"Invite": "Einladen",
|
||||
"Invitation sent to {{email}}.": "Einladung an {{email}} gesendet.",
|
||||
"Invite new members to {{title}}": "Neue Mitglieder zu {{title}} einladen",
|
||||
"Invited": "Eingeladen",
|
||||
"It is the card information about the document.": "Es handelt sich um die Karteninformationen zum Dokument.",
|
||||
"It is the document title": "Es ist der Titel des Dokuments",
|
||||
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Es scheint, dass die von Ihnen gesuchte Seite nicht existiert oder nicht korrekt angezeigt werden kann.",
|
||||
"Language": "Sprache",
|
||||
"Last update: {{update}}": "Zuletzt aktualisiert: {{update}}",
|
||||
"Legal Notice": "Impressum",
|
||||
"Legal notice": "Impressum",
|
||||
"Link Copied !": "Link kopiert!",
|
||||
"Link parameters": "Link-Parameter",
|
||||
"List invitation card": "Einladungsliste anzeigen",
|
||||
"List members card": "Mitgliederliste anzeigen",
|
||||
"Load more": "Mehr anzeigen",
|
||||
"Login": "Anmelden",
|
||||
"Logout": "Abmelden",
|
||||
"Modal confirmation to restore the version": "Modale Bestätigung um die Version wiederherzustellen",
|
||||
"More docs": "Weitere Dokumente",
|
||||
"My docs": "Meine Dokumente",
|
||||
"Name": "Name",
|
||||
"New doc": "Neues Dokument",
|
||||
"No active search": "Keine aktive Suche",
|
||||
"No document found": "Kein Dokument gefunden",
|
||||
"No documents found": "Keine Dokumente gefunden",
|
||||
"No editor found": "Kein Editor gefunden",
|
||||
"No versions": "Keine Versionen",
|
||||
"OK": "OK",
|
||||
"Offline ?!": "Offline?!",
|
||||
"Only invited people can access": "Nur eingeladene Personen haben Zugriff",
|
||||
"Only for people with access": "Nur für Personen mit Zugriff",
|
||||
"Open the document options": "Öffnen Sie die Dokumentoptionen",
|
||||
"Open the header menu": "Öffne das Kopfzeilen-Menü",
|
||||
"Ouch !": "Autsch!",
|
||||
"Owner": "Besitzer",
|
||||
"PDF": "PDF",
|
||||
"Pending invitations": "Ausstehende Einladungen",
|
||||
"Personal data and cookies": "Personenbezogene Daten und Cookies",
|
||||
"Pin": "Anheften",
|
||||
"Pinned documents": "Angepinnte Dokumente",
|
||||
"Private": "Privat",
|
||||
"Public": "Öffentlich",
|
||||
"Public document": "Öffentliches Dokument",
|
||||
"Quick search input": "Schnellsuche-Eingabe",
|
||||
"Read only, you cannot edit this document.": "Nur lesen: Sie können dieses Dokument nicht bearbeiten.",
|
||||
"Reader": "Leser",
|
||||
"Reading": "Lesen",
|
||||
"Remove": "Löschen",
|
||||
"Rename": "Umbenennen",
|
||||
"Rephrase": "Umformulieren",
|
||||
"Restore": "Wiederherstellen",
|
||||
"Search": "Suchen",
|
||||
"Search modal": "Suche Modal",
|
||||
"Search user result": "Suchergebnis",
|
||||
"Select a document": "Dokument auswählen",
|
||||
"Select a version on the right to restore": "Wählen Sie rechts eine Version zum Wiederherstellen aus",
|
||||
"Role": "Rolle",
|
||||
"Search by email": "Nach E-Mail suchen",
|
||||
"Share": "Teilen",
|
||||
"Share modal": "Teilen-Modal",
|
||||
"Share the document": "Dokument teilen",
|
||||
"Share with {{count}} users_many": "Teilen mit {{count}} Benutzern",
|
||||
"Share with {{count}} users_one": "Teilen mit {{count}} Benutzern",
|
||||
"Share with {{count}} users_other": "Teilen mit {{count}} Benutzern",
|
||||
"Shared with me": "Mit mir geteilt",
|
||||
"Something bad happens, please retry.": "Etwas ist schiefgelaufen, bitte versuchen Sie es erneut.",
|
||||
"Summarize": "Zusammenfassen",
|
||||
"Summary": "Zusammenfassung",
|
||||
"Table of contents": "Inhaltsverzeichnis",
|
||||
"Template": "Vorlage",
|
||||
"The document has been deleted.": "Das Dokument wurde gelöscht.",
|
||||
"The document visibility has been updated.": "Die Sichtbarkeit des Dokuments wurde aktualisiert.",
|
||||
"The invitation has been removed.": "Die Einladung wurde zurückgenommen.",
|
||||
"The member has been removed from the document": "Das Mitglied wurde aus dem Dokument entfernt",
|
||||
"The role has been updated": "Die Rolle wurde aktualisiert",
|
||||
"The role has been updated.": "Die Rolle wurde aktualisiert.",
|
||||
"This accessibility statement applies to the site hosted on": "Diese Erklärung zur Barrierefreiheit gilt für die gehostete Seite",
|
||||
"This site does not display a cookie consent banner, why?": "",
|
||||
"Too many requests. Please wait 60 seconds.": "Zu viele Anfragen. Bitte warten Sie 60 Sekunden.",
|
||||
"Type a name or email": "Geben Sie einen Namen oder eine E-Mail-Adresse ein",
|
||||
"Type the name of a document": "Geben Sie den Namen eines Dokuments ein",
|
||||
"Unless otherwise stated, all content on this site is under": "Sofern nicht anders angegeben, steht der gesamte Inhalt dieser Website unter",
|
||||
"Unpin": "Lösen",
|
||||
"Untitled document": "Unbenanntes Dokument",
|
||||
"Updated at": "Aktualisiert am",
|
||||
"Upload your docs to a Microsoft Word, Open Office or PDF document.": "Laden Sie Ihre Dokumente zu einem Microsoft Word, Open Office oder PDF Dokument hoch.",
|
||||
"Use as prompt": "Als Prompt verwenden",
|
||||
"User {{email}} added to the document.": "Benutzer {{email}} wurde dem Dokument hinzugefügt.",
|
||||
"Validate": "Bestätigen",
|
||||
"Version history": "Versionsverlauf",
|
||||
"Version restored successfully": "Version erfolgreich wiederhergestellt",
|
||||
"Visibility": "Sichtbarkeit",
|
||||
"Visibility mode": "Sichtbarkeitseinstellungen",
|
||||
"Warning": "Warnung",
|
||||
"We didn't find a mail matching, try to be more accurate": "Wir haben keine übereinstimmende E-Mail gefunden, versuchen Sie genauer zu sein",
|
||||
"We try to respond within 2 working days.": "Wir versuchen, innerhalb von 2 Arbeitstagen zu antworten.",
|
||||
"Word / Open Office": "Word / Open Office",
|
||||
"You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.": "Sie sind der einzige Besitzer dieser Gruppe. Machen Sie ein anderes Mitglied zum Gruppenbesitzer, bevor Sie Ihre eigene Rolle ändern oder aus Ihrem Dokument entfernen können.",
|
||||
"You cannot update the role or remove other owner.": "Sie können die Rolle nicht aktualisieren oder einen anderen Besitzer entfernen.",
|
||||
"Your current document will revert to this version.": "Ihr aktuelles Dokument wird auf diese Version zurückgesetzt.",
|
||||
@@ -156,23 +96,21 @@
|
||||
"AI seems busy! Please try again.": "L'IA semble occupée ! Veuillez réessayer.",
|
||||
"Accessibility": "Accessibilité",
|
||||
"Accessibility statement": "Déclaration d'accessibilité",
|
||||
"Add": "Ajouter",
|
||||
"Address:": "Adresse :",
|
||||
"Administrator": "Administrateur",
|
||||
"All docs": "Tous les documents",
|
||||
"Anonymous": "Anonyme",
|
||||
"Anyone with the link can edit the document": "N'importe qui avec le lien peut éditer le document",
|
||||
"Anyone with the link can edit the document if they are logged in": "N'importe qui avec le lien peut éditer le document à condition qu'il soit connecté",
|
||||
"Anyone with the link can see the document": "N'importe qui avec le lien peut voir le document",
|
||||
"Anyone with the link can view the document if they are logged in": "N'importe qui avec le lien peut voir le document à condition qu'il soit connecté",
|
||||
"Anyone on the internet with the link can view": "Les personnes disposant du lien peuvent y accéder",
|
||||
"Are you sure you want to delete the document \"{{title}}\"?": "Êtes-vous sûr de vouloir supprimer le document \"{{title}}\" ?",
|
||||
"Authenticated": "Authentifié",
|
||||
"Back to home page": "Retour à l'accueil",
|
||||
"Can read and edit": "Peut lire et éditer",
|
||||
"Can't load this page, please check your internet connection.": "Impossible de charger cette page, veuillez vérifier votre connexion Internet.",
|
||||
"Cancel": "Annuler",
|
||||
"Choose a role": "Choisissez un rôle",
|
||||
"Close the modal": "Fermer la modale",
|
||||
"Compliance status": "État de conformité",
|
||||
"Confirm deletion": "Confirmer la suppression",
|
||||
"Connected": "Connecté",
|
||||
"Content modal to delete document": "Contenu modal pour supprimer le document",
|
||||
"Content modal to export the document": "Contenu modal pour exporter le document",
|
||||
"Convert Markdown": "Convertir le Markdown",
|
||||
@@ -183,38 +121,35 @@
|
||||
"Copyright": "Copyright",
|
||||
"Correct": "Corriger",
|
||||
"Defender of Rights - Free response - 71120 75342 Paris CEDEX 07": "Défenseur des droits - Réponse gratuite - 71120 75342 Paris CEDEX 07",
|
||||
"Delete": "Supprimer",
|
||||
"Delete a doc": "Supprimer un doc",
|
||||
"Delete document": "Supprimer le document",
|
||||
"Deleting the document \"{{title}}\"": "Suppression du document \"{{title}}\"",
|
||||
"Doc visibility card": "Carte de visibilité du doc",
|
||||
"Docs": "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 owner": "Propriétaire du document",
|
||||
"Document title updated successfully": "Titre du document mis à jour avec succès",
|
||||
"Download": "Télécharger",
|
||||
"E-mail:": "E-mail:",
|
||||
"Edition": "Édition",
|
||||
"Editor": "Éditeur",
|
||||
"Editor unavailable": "Éditeur indisponible",
|
||||
"Error during delete invitation": "Erreur lors de la suppression de l'invitation",
|
||||
"Error during invitation update": "Erreur lors de la mise à jour de l'invitation",
|
||||
"Error during update invitation": "Erreur lors de la mise à jour de l'invitation",
|
||||
"Error while deleting invitation": "Erreur lors de la suppression de l'invitation",
|
||||
"Established on December 20, 2023.": "Établi le 20 décembre 2023.",
|
||||
"Export": "Exporter",
|
||||
"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 copy to clipboard": "Échec de la copie dans le presse-papier",
|
||||
"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",
|
||||
"Format": "Format",
|
||||
"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.",
|
||||
"History": "Historique",
|
||||
"How people can interact with the document": "Comment les gens peuvent interagir avec le document",
|
||||
"If a member is editing, his works can be lost.": "Si un membre est en train d'éditer, ses travaux peuvent être perdus.",
|
||||
"If you are unable to access a content or a service, you can contact the person responsible for https://lasuite.numerique.gouv.fr to be directed to an accessible alternative or to obtain the content in another form.": "Si vous ne pouvez pas accéder à un contenu ou à un service, vous pouvez contacter la personne responsable de https://lasuite. umerique.gouv.fr pour être dirigé vers une alternative accessible ou pour obtenir le contenu sous une autre forme.",
|
||||
"Illustration:": "Illustration :",
|
||||
"Improvement and contact": "Amélioration et contact",
|
||||
"Invite": "Inviter",
|
||||
"Invitation sent to {{email}}.": "Invitation envoyée à {{email}}.",
|
||||
"Invite new members to {{title}}": "Invitez de nouveaux membres à rejoindre {{title}}",
|
||||
"Invited": "Invité",
|
||||
"It is the card information about the document.": "Il s'agit de la carte d'information du document.",
|
||||
"It is the document title": "Il s'agit du titre 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.",
|
||||
@@ -224,7 +159,6 @@
|
||||
"Legal Notice": "Mentions Legales",
|
||||
"Legal notice": "Mention légale",
|
||||
"Link Copied !": "Lien copié !",
|
||||
"Link parameters": "Paramètres du lien",
|
||||
"List invitation card": "Carte de liste d'invitation",
|
||||
"List members card": "Carte liste des membres",
|
||||
"Load more": "Afficher plus",
|
||||
@@ -236,57 +170,50 @@
|
||||
"My docs": "Mes documents",
|
||||
"Name": "Nom",
|
||||
"New doc": "Nouveau doc",
|
||||
"No active search": "Aucune recherche active",
|
||||
"No document found": "Aucun document trouvé",
|
||||
"No documents found": "Aucun document trouvé",
|
||||
"No editor found": "Pas d'éditeur trouvé",
|
||||
"No versions": "Aucune version",
|
||||
"Nothing exceptional, no special privileges related to a .gouv.fr.": "Rien d'exceptionnel, pas de privilèges spéciaux liés à un .gouv.fr.",
|
||||
"OK": "OK",
|
||||
"Offline ?!": "Hors-ligne ?!",
|
||||
"Only invited people can access": "Seules les personnes invitées peuvent accéder",
|
||||
"Only for authenticated users": "Uniquement pour les utilisateurs authentifiés",
|
||||
"Only for people with access": "Seulement pour les personnes avec accès",
|
||||
"Open the document options": "Ouvrir les options du document",
|
||||
"Open the header menu": "Ouvrir le menu d'en-tête",
|
||||
"Ouch !": "Aïe !",
|
||||
"Owner": "Propriétaire",
|
||||
"PDF": "PDF",
|
||||
"Pending invitations": "Invitations en attente",
|
||||
"Personal data and cookies": "Données personnelles et cookies",
|
||||
"Pin": "Épingler",
|
||||
"Pinned documents": "Documents épinglés",
|
||||
"Private": "Privé",
|
||||
"Public": "Public",
|
||||
"Public document": "Document public",
|
||||
"Publication Director": "Directeur de la publication",
|
||||
"Publisher": "Éditeur",
|
||||
"Quick search input": "Saisie de recherche rapide",
|
||||
"Read only": "Lecture seule",
|
||||
"Read only, you cannot edit this document.": "En lecture seule, vous ne pouvez pas éditer ce document.",
|
||||
"Reader": "Lecteur",
|
||||
"Reading": "Lecture seul",
|
||||
"Remedies": "Voie de recours",
|
||||
"Remove": "Supprimer",
|
||||
"Rename": "Renommer",
|
||||
"Rephrase": "Reformuler",
|
||||
"Restore": "Restaurer",
|
||||
"Search": "Rechercher",
|
||||
"Search modal": "Modale de partage",
|
||||
"Search user result": "Résultat de la recherche utilisateur",
|
||||
"Select a document": "Sélectionnez un document",
|
||||
"Restricted": "Restreint",
|
||||
"Role": "Rôle",
|
||||
"Search by email": "Recherche par email",
|
||||
"Select a version on the right to restore": "Sélectionnez une version à droite à restaurer",
|
||||
"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",
|
||||
"Share modal": "Modale de partage",
|
||||
"Share the document": "Partager le document",
|
||||
"Share with {{count}} users_many": "Partager avec {{count}} utilisateurs",
|
||||
"Share with {{count}} users_one": "Partager avec {{count}} utilisateur",
|
||||
"Share with {{count}} users_other": "Partager avec {{count}} utilisateurs",
|
||||
"Shared with me": "Partagés avec moi",
|
||||
"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).",
|
||||
"Summarize": "Résumer",
|
||||
"Summary": "Sommaire",
|
||||
"Table of contents": "Table des matières",
|
||||
"Template": "Template",
|
||||
"The document has been deleted.": "Le document a bien été supprimé.",
|
||||
"The document visibility 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",
|
||||
"The role has been updated.": "Le rôle a été mis à jour.",
|
||||
"The team in charge of the digital workspace \"La Suite numérique\" can be contacted directly at": "L'équipe responsable de l'espace de travail numérique \"La Suite numérique\" peut être contactée directement à l'adresse",
|
||||
"This accessibility statement applies to the site hosted on": "Cette déclaration d'accessibilité s'applique au site hébergé sur",
|
||||
"This allows us to measure the number of visits and understand which pages are the most viewed.": "Cela nous permet de mesurer le nombre de visites et de comprendre quelles pages sont les plus consultées.",
|
||||
@@ -295,19 +222,18 @@
|
||||
"This site places a small text file (a \"cookie\") on your computer when you visit it.": "Ce site place un petit fichier texte (un « cookie ») sur votre ordinateur lorsque vous le visitez.",
|
||||
"This will protect your privacy, but will also prevent the owner from learning from your actions and creating a better experience for you and other users.": "Cela protégera votre vie privée, mais empêchera également le propriétaire d'apprendre de vos actions et de créer une meilleure expérience pour vous et les autres utilisateurs.",
|
||||
"Too many requests. Please wait 60 seconds.": "Trop de demandes. Veuillez patienter 60 secondes.",
|
||||
"Type a name or email": "Tapez un nom ou un email",
|
||||
"Type the name of a document": "Tapez le nom d'un document",
|
||||
"Unless otherwise stated, all content on this site is under": "Sauf mention contraire, tout le contenu de ce site est sous",
|
||||
"Unpin": "Désépingler",
|
||||
"Untitled document": "Document sans titre",
|
||||
"Updated at": "Mise à jour le",
|
||||
"Upload your docs to a Microsoft Word, Open Office or PDF document.": "Téléchargez vos documents dans un document Microsoft Word, Open Office ou PDF.",
|
||||
"Use as prompt": "Utiliser comme un prompt",
|
||||
"User {{email}} added to the document.": "L'utilisateur {{email}} a été ajouté au document.",
|
||||
"Validate": "Valider",
|
||||
"Version history": "Historique des versions",
|
||||
"Version restored successfully": "Version restaurée avec succès",
|
||||
"Visibility": "Visibilité",
|
||||
"Visibility mode": "Mode de visibilité",
|
||||
"Warning": "Attention",
|
||||
"We didn't find a mail matching, try to be more accurate": "Nous n'avons pas trouvé de correspondance par mail, essayez d'être plus précis",
|
||||
"We simply comply with the law, which states that certain audience measurement tools, properly configured to respect privacy, are exempt from prior authorization.": "Nous nous conformons simplement à la loi, qui stipule que certains outils de mesure d’audience, correctement configurés pour respecter la vie privée, sont exemptés de toute autorisation préalable.",
|
||||
"We try to respond within 2 working days.": "Nous essayons de répondre dans les 2 jours ouvrables.",
|
||||
"Word / Open Office": "Word / Open Office",
|
||||
|
||||
@@ -3,7 +3,6 @@ import { css } from 'styled-components';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Footer } from '@/features/footer';
|
||||
import { Header } from '@/features/header';
|
||||
import { HEADER_HEIGHT } from '@/features/header/conf';
|
||||
import { LeftPanel } from '@/features/left-panel';
|
||||
@@ -12,13 +11,14 @@ import { useResponsiveStore } from '@/stores';
|
||||
|
||||
type MainLayoutProps = {
|
||||
backgroundColor?: 'white' | 'grey';
|
||||
withoutFooter?: boolean;
|
||||
|
||||
withoutLeftPanel?: boolean;
|
||||
};
|
||||
|
||||
export function MainLayout({
|
||||
children,
|
||||
backgroundColor = 'white',
|
||||
withoutFooter = false,
|
||||
withoutLeftPanel = false,
|
||||
}: PropsWithChildren<MainLayoutProps>) {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
@@ -33,7 +33,7 @@ export function MainLayout({
|
||||
$margin={{ top: `${HEADER_HEIGHT}px` }}
|
||||
$width="100%"
|
||||
>
|
||||
<LeftPanel />
|
||||
{!withoutLeftPanel && <LeftPanel />}
|
||||
<Box
|
||||
as="main"
|
||||
id={MAIN_LAYOUT_ID}
|
||||
@@ -57,7 +57,6 @@ export function MainLayout({
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
{!withoutFooter && <Footer />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Box, Text, TextErrors } from '@/components';
|
||||
import { Box, Text } from '@/components';
|
||||
import { TextErrors } from '@/components/TextErrors';
|
||||
import { useAuthStore } from '@/core/auth';
|
||||
import { DocEditor } from '@/features/docs/doc-editor';
|
||||
import {
|
||||
@@ -33,7 +34,7 @@ export function DocLayout() {
|
||||
<meta name="robots" content="noindex" />
|
||||
</Head>
|
||||
|
||||
<MainLayout withoutFooter>
|
||||
<MainLayout>
|
||||
<DocPage id={id} />
|
||||
</MainLayout>
|
||||
</>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useSearchParams } from 'next/navigation';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import { DocDefaultFilter } from '@/features/docs';
|
||||
import { DocsGrid } from '@/features/docs/docs-grid';
|
||||
import { DocsGrid } from '@/features/docs/docs-grid/components/DocsGrid';
|
||||
import { MainLayout } from '@/layouts';
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
|
||||
5
src/frontend/apps/impress/src/pages/home.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import HomeContent from '@/features/home/components/HomeContent';
|
||||
|
||||
export default function Home() {
|
||||
return <HomeContent />;
|
||||
}
|
||||
@@ -1,3 +1,16 @@
|
||||
import Docs from './docs';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default Docs;
|
||||
import { useAuthStore } from '@/core';
|
||||
import HomeContent from '@/features/home/components/HomeContent';
|
||||
|
||||
export default function Index() {
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
|
||||
if (auth.userData) {
|
||||
router.push('/docs/');
|
||||
return null;
|
||||
}
|
||||
|
||||
return <HomeContent />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impress",
|
||||
"version": "2.0.0",
|
||||
"version": "1.10.0",
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eslint-config-impress",
|
||||
"version": "2.0.0",
|
||||
"version": "1.10.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js ."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "packages-i18n",
|
||||
"version": "2.0.0",
|
||||
"version": "1.10.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"extract-translation": "yarn extract-translation:impress",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server-y-provider",
|
||||
"version": "2.0.0",
|
||||
"version": "1.10.0",
|
||||
"description": "Y.js provider for docs",
|
||||
"repository": "https://github.com/numerique-gouv/impress",
|
||||
"license": "MIT",
|
||||
@@ -35,8 +35,8 @@
|
||||
"@types/node": "*",
|
||||
"@types/supertest": "6.0.2",
|
||||
"@types/ws": "8.5.13",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-config-impress": "*",
|
||||
"cross-env": "7.0.3",
|
||||
"jest": "29.7.0",
|
||||
"nodemon": "3.1.9",
|
||||
"supertest": "7.0.0",
|
||||
|
||||
@@ -93,4 +93,4 @@ releases:
|
||||
environments:
|
||||
dev:
|
||||
values:
|
||||
- version: 2.0.0
|
||||
- version: 1.10.0
|
||||
|
||||