Compare commits
28 Commits
feature/cu
...
hackathon/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ee9f9152c | ||
|
|
b7914dfeef | ||
|
|
1b2f74660b | ||
|
|
25491aeb06 | ||
|
|
6bb56e6b72 | ||
|
|
cdf157c72b | ||
|
|
e26fcdf985 | ||
|
|
5cc4b07cf6 | ||
|
|
0cfc242e09 | ||
|
|
a6b3cfdb0c | ||
|
|
5ead18c94c | ||
|
|
5eeb8cae5c | ||
|
|
68bf024005 | ||
|
|
fdd1068c90 | ||
|
|
ba695bf647 | ||
|
|
27e7aec193 | ||
|
|
58b712a1de | ||
|
|
08f9036523 | ||
|
|
ebe3efc8f7 | ||
|
|
66fbf27913 | ||
|
|
20e4a4e42a | ||
|
|
1aa4844eeb | ||
|
|
4bb9c092cb | ||
|
|
c493eb8924 | ||
|
|
40fdf97520 | ||
|
|
91b10e75dd | ||
|
|
7a6da10e1c | ||
|
|
004e8ec645 |
5
.github/workflows/crowdin_download.yml
vendored
@@ -7,10 +7,11 @@ on:
|
||||
- 'release/**'
|
||||
|
||||
jobs:
|
||||
install-front:
|
||||
uses: ./.github/workflows/front-dependencies-installation.yml
|
||||
install-dependencies:
|
||||
uses: ./.github/workflows/dependencies.yml
|
||||
with:
|
||||
node_version: '20.x'
|
||||
with-front-dependencies-installation: true
|
||||
|
||||
synchronize-with-crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
15
.github/workflows/crowdin_upload.yml
vendored
@@ -7,13 +7,15 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
install-front:
|
||||
uses: ./.github/workflows/front-dependencies-installation.yml
|
||||
install-dependencies:
|
||||
uses: ./.github/workflows/dependencies.yml
|
||||
with:
|
||||
node_version: '20.x'
|
||||
with-front-dependencies-installation: true
|
||||
with-build_mails: true
|
||||
|
||||
synchronize-with-crowdin:
|
||||
needs: install-front
|
||||
needs: install-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -29,6 +31,13 @@ jobs:
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .
|
||||
working-directory: src/backend
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@v4
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
fail-on-cache-miss: true
|
||||
- name: Install gettext
|
||||
run: |
|
||||
sudo apt-get update
|
||||
|
||||
85
.github/workflows/dependencies.yml
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
name: Dependency reusable workflow
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
node_version:
|
||||
required: false
|
||||
default: '20.x'
|
||||
type: string
|
||||
with-front-dependencies-installation:
|
||||
type: boolean
|
||||
default: false
|
||||
with-build_mails:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
front-dependencies-installation:
|
||||
if: ${{ inputs.with-front-dependencies-installation == true }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v4
|
||||
id: front-node_modules
|
||||
with:
|
||||
path: "src/frontend/**/node_modules"
|
||||
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
|
||||
- name: Setup Node.js
|
||||
if: steps.front-node_modules.outputs.cache-hit != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node_version }}
|
||||
- name: Install dependencies
|
||||
if: steps.front-node_modules.outputs.cache-hit != 'true'
|
||||
run: cd src/frontend/ && yarn install --frozen-lockfile
|
||||
- name: Cache install frontend
|
||||
if: steps.front-node_modules.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: "src/frontend/**/node_modules"
|
||||
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
|
||||
|
||||
build-mails:
|
||||
if: ${{ inputs.with-build_mails == true }}
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/mail
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@v4
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node_version }}
|
||||
|
||||
- name: Install yarn
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Install node dependencies
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build mails
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
run: yarn build
|
||||
|
||||
- name: Cache mail templates
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
@@ -1,36 +0,0 @@
|
||||
name: Install frontend installation reusable workflow
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
node_version:
|
||||
required: false
|
||||
default: '20.x'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
front-dependencies-installation:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v4
|
||||
id: front-node_modules
|
||||
with:
|
||||
path: "src/frontend/**/node_modules"
|
||||
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
|
||||
- name: Setup Node.js
|
||||
if: steps.front-node_modules.outputs.cache-hit != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node_version }}
|
||||
- name: Install dependencies
|
||||
if: steps.front-node_modules.outputs.cache-hit != 'true'
|
||||
run: cd src/frontend/ && yarn install --frozen-lockfile
|
||||
- name: Cache install frontend
|
||||
if: steps.front-node_modules.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: "src/frontend/**/node_modules"
|
||||
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
|
||||
11
.github/workflows/impress-frontend.yml
vendored
@@ -10,13 +10,14 @@ on:
|
||||
|
||||
jobs:
|
||||
|
||||
install-front:
|
||||
uses: ./.github/workflows/front-dependencies-installation.yml
|
||||
install-dependencies:
|
||||
uses: ./.github/workflows/dependencies.yml
|
||||
with:
|
||||
node_version: '20.x'
|
||||
with-front-dependencies-installation: true
|
||||
|
||||
test-front:
|
||||
needs: install-front
|
||||
needs: install-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -39,7 +40,7 @@ jobs:
|
||||
|
||||
lint-front:
|
||||
runs-on: ubuntu-latest
|
||||
needs: install-front
|
||||
needs: install-dependencies
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -60,7 +61,7 @@ jobs:
|
||||
|
||||
test-e2e-chromium:
|
||||
runs-on: ubuntu-latest
|
||||
needs: install-front
|
||||
needs: install-dependencies
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
48
.github/workflows/impress.yml
vendored
@@ -9,6 +9,11 @@ on:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
install-dependencies:
|
||||
uses: ./.github/workflows/dependencies.yml
|
||||
with:
|
||||
with-build_mails: true
|
||||
|
||||
lint-git:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request' # Makes sense only for pull requests
|
||||
@@ -56,46 +61,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build-mails:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/mail
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@v4
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
|
||||
- name: Install yarn
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Install node dependencies
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build mails
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
run: yarn build
|
||||
|
||||
- name: Cache mail templates
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
|
||||
lint-back:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
@@ -121,7 +86,7 @@ jobs:
|
||||
|
||||
test-back:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-mails
|
||||
needs: install-dependencies
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -169,6 +134,7 @@ jobs:
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Start MinIO
|
||||
run: |
|
||||
|
||||
11
CHANGELOG.md
@@ -10,8 +10,17 @@ and this project adheres to
|
||||
## [Unreleased]
|
||||
|
||||
## Added
|
||||
|
||||
- 📝(doc) Add security.md and codeofconduct.md #604
|
||||
- ✨(frontend) add Alert, Quote, and Divider blocks to the editor #566
|
||||
- ✨(frontend) add home page #553
|
||||
- ✨(frontend) cursor display on activity #609
|
||||
- ✨(frontend) Add export page break #623
|
||||
|
||||
## Fixed
|
||||
|
||||
🌐(CI) Fix email partially translated #616
|
||||
- 🐛(frontend) fix cursor breakline #609
|
||||
- 🐛(frontend) fix style pdf export #609
|
||||
|
||||
|
||||
## [2.1.0] - 2025-01-29
|
||||
|
||||
@@ -21,8 +21,8 @@ services:
|
||||
- MINIO_ROOT_USER=impress
|
||||
- MINIO_ROOT_PASSWORD=password
|
||||
ports:
|
||||
- '9000:9000'
|
||||
- '9001:9001'
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
entrypoint: ""
|
||||
command: minio server --console-address :9001 /data
|
||||
volumes:
|
||||
@@ -59,11 +59,11 @@ services:
|
||||
- ./src/backend:/app
|
||||
- ./data/static:/data/static
|
||||
depends_on:
|
||||
- postgresql
|
||||
- mailcatcher
|
||||
- redis
|
||||
- createbuckets
|
||||
|
||||
- postgresql
|
||||
- mailcatcher
|
||||
- redis
|
||||
- createbuckets
|
||||
|
||||
celery-dev:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
image: impress:backend-development
|
||||
@@ -122,7 +122,7 @@ services:
|
||||
|
||||
frontend-dev:
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
build:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./src/frontend/Dockerfile
|
||||
target: frontend-production
|
||||
@@ -151,13 +151,13 @@ services:
|
||||
image: node:18
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
environment:
|
||||
HOME: /tmp
|
||||
HOME: /tmp
|
||||
volumes:
|
||||
- ".:/app"
|
||||
|
||||
y-provider:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
build:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./src/frontend/servers/y-provider/Dockerfile
|
||||
target: y-provider
|
||||
@@ -169,6 +169,7 @@ services:
|
||||
|
||||
kc_postgresql:
|
||||
image: postgres:14.3
|
||||
platform: linux/amd64
|
||||
ports:
|
||||
- "5433:5432"
|
||||
env_file:
|
||||
@@ -196,7 +197,7 @@ services:
|
||||
KC_DB_PASSWORD: pass
|
||||
KC_DB_USERNAME: impress
|
||||
KC_DB_SCHEMA: public
|
||||
PROXY_ADDRESS_FORWARDING: 'true'
|
||||
PROXY_ADDRESS_FORWARDING: "true"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
|
||||
@@ -10,6 +10,10 @@ LOGGING_LEVEL_HANDLERS_CONSOLE=INFO
|
||||
LOGGING_LEVEL_LOGGERS_ROOT=INFO
|
||||
LOGGING_LEVEL_LOGGERS_APP=INFO
|
||||
|
||||
# Y-Provider
|
||||
Y_PROVIDER_API_KEY="yprovider-api-key"
|
||||
Y_PROVIDER_API_BASE_URL=http://y-provider:4444/api/
|
||||
|
||||
# Python
|
||||
PYTHONPATH=/app
|
||||
|
||||
@@ -54,6 +58,9 @@ AI_BASE_URL=https://openaiendpoint.com
|
||||
AI_API_KEY=password
|
||||
AI_MODEL=llama
|
||||
|
||||
# Accessibility API
|
||||
ACCESSIBILITY_API_BASE_URL=https://localhost:8000
|
||||
|
||||
# Collaboration
|
||||
COLLABORATION_API_URL=http://nginx:8083/collaboration/api/
|
||||
COLLABORATION_SERVER_ORIGIN=http://localhost:3000
|
||||
|
||||
@@ -16,6 +16,7 @@ from core.services.converter_services import (
|
||||
ConversionError,
|
||||
YdocConverter,
|
||||
)
|
||||
from core.services.ai_services import AIService
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
@@ -306,7 +307,7 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
|
||||
if user:
|
||||
email = user.email
|
||||
language = user.language or language
|
||||
|
||||
|
||||
try:
|
||||
document_content = YdocConverter().convert_markdown(
|
||||
validated_data["content"]
|
||||
@@ -568,6 +569,56 @@ class AITranslateSerializer(serializers.Serializer):
|
||||
if len(value.strip()) == 0:
|
||||
raise serializers.ValidationError("Text field cannot be empty.")
|
||||
return value
|
||||
|
||||
|
||||
class AIPdfTranscribeSerializer(serializers.Serializer):
|
||||
"""Serializer for AI PDF transcribe requests."""
|
||||
|
||||
pdfUrl = serializers.CharField(required=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize with user."""
|
||||
self.user = kwargs.pop('user', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def validate_pdfUrl(self, value):
|
||||
"""Ensure the pdfUrl field is a valid URL."""
|
||||
if not value.startswith(settings.MEDIA_BASE_URL):
|
||||
raise serializers.ValidationError("Invalid PDF URL format.")
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create a new document for the transcribed content."""
|
||||
if not self.user:
|
||||
raise serializers.ValidationError("User is required")
|
||||
|
||||
# Get the transcribed content from AI service
|
||||
pdf_url = validated_data["pdfUrl"]
|
||||
response = AIService().transcribe_pdf(pdf_url)
|
||||
|
||||
try:
|
||||
# Convert the markdown content to YDoc format
|
||||
document_content = YdocConverter().convert_markdown(response)
|
||||
except ConversionError as err:
|
||||
raise serializers.ValidationError(
|
||||
{"content": [f"Could not convert transcribed content: {str(err)}"]}
|
||||
) from err
|
||||
|
||||
# Create the document as root node with converted content
|
||||
document = models.Document.add_root(
|
||||
title="PDF Transcription",
|
||||
content=document_content,
|
||||
creator=self.user,
|
||||
)
|
||||
|
||||
# Create owner access for the user
|
||||
models.DocumentAccess.objects.create(
|
||||
document=document,
|
||||
role=models.RoleChoices.OWNER,
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
return document
|
||||
|
||||
|
||||
class MoveDocumentSerializer(serializers.Serializer):
|
||||
|
||||
@@ -1079,6 +1079,41 @@ class DocumentViewSet(
|
||||
|
||||
return drf.response.Response(response, status=drf.status.HTTP_200_OK)
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=True,
|
||||
methods=["post"],
|
||||
name="Just proxy ai call",
|
||||
url_path="ai-proxy"
|
||||
)
|
||||
def ai_proxy(self, request, *args, **kwargs):
|
||||
"""
|
||||
POST /api/v1.0/documents/<resource_id>/ai-transform
|
||||
with expected data:
|
||||
- text: str
|
||||
- action: str [prompt, correct, rephrase, summarize]
|
||||
Return JSON response with the processed text.
|
||||
"""
|
||||
print('PROXY 1')
|
||||
# Check permissions first
|
||||
# self.get_object()
|
||||
|
||||
print('PROXY 2')
|
||||
print(request.data)
|
||||
# serializer = serializers.AITransformSerializer(data=request.data)
|
||||
# serializer.is_valid(raise_exception=True)
|
||||
|
||||
print('PROXY 3')
|
||||
system_content = request.data["system"]
|
||||
text = request.data["text"]
|
||||
|
||||
print('PROXY 4')
|
||||
response = AIService().call_proxy(system_content, text)
|
||||
|
||||
print('PROXY 5')
|
||||
print(response)
|
||||
|
||||
return drf.response.Response(response, status=drf.status.HTTP_200_OK)
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=True,
|
||||
methods=["post"],
|
||||
@@ -1107,6 +1142,32 @@ class DocumentViewSet(
|
||||
|
||||
return drf.response.Response(response, status=drf.status.HTTP_200_OK)
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=True,
|
||||
methods=["post"],
|
||||
name="Transcribe PDF with AI",
|
||||
url_path="ai-pdf-transcribe",
|
||||
throttle_classes=[utils.AIDocumentRateThrottle, utils.AIUserRateThrottle],
|
||||
)
|
||||
def ai_pdf_transcribe(self, request, *args, **kwargs):
|
||||
"""
|
||||
POST /api/v1.0/documents/<resource_id>/ai-pdf-transcribe
|
||||
with expected data:
|
||||
- pdfUrl: str
|
||||
Return JSON response with the new document ID containing the transcription.
|
||||
"""
|
||||
serializer = serializers.AIPdfTranscribeSerializer(
|
||||
data=request.data,
|
||||
user=request.user
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
document = serializer.save()
|
||||
|
||||
return drf.response.Response(
|
||||
{"document_id": str(document.id)},
|
||||
status=drf.status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
|
||||
class DocumentAccessViewSet(
|
||||
ResourceAccessViewsetMixin,
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
import requests
|
||||
import botocore
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
from core import enums
|
||||
from core.models import Document
|
||||
|
||||
AI_ACTIONS = {
|
||||
"prompt": (
|
||||
@@ -55,6 +60,22 @@ class AIService:
|
||||
raise ImproperlyConfigured("AI configuration not set")
|
||||
self.client = OpenAI(base_url=settings.AI_BASE_URL, api_key=settings.AI_API_KEY)
|
||||
|
||||
def call_proxy(self, system_content, text):
|
||||
messages = [
|
||||
{"role": "system", "content": system_content},
|
||||
{"role": "user", "content": text},
|
||||
]
|
||||
print('REQUEST', messages)
|
||||
response = self.client.chat.completions.create(
|
||||
model=settings.AI_MODEL,
|
||||
messages=messages,
|
||||
)
|
||||
|
||||
print('RESPONSE', response)
|
||||
content = response.choices[0].message.content
|
||||
print('CONTENT', content)
|
||||
return content
|
||||
|
||||
def call_ai_api(self, system_content, text):
|
||||
"""Helper method to call the OpenAI API and process the response."""
|
||||
response = self.client.chat.completions.create(
|
||||
@@ -96,3 +117,26 @@ class AIService:
|
||||
language_display = enums.ALL_LANGUAGES.get(language, language)
|
||||
system_content = AI_TRANSLATE.format(language=language_display)
|
||||
return self.call_ai_api(system_content, text)
|
||||
|
||||
def transcribe_pdf(self, pdf_url):
|
||||
"""Transcribe PDF using the accessibility hackathon API and create a new document."""
|
||||
try:
|
||||
media_prefix = os.path.join(settings.MEDIA_BASE_URL, "media")
|
||||
key = pdf_url[len(media_prefix):]
|
||||
|
||||
pdf_response = default_storage.connection.meta.client.get_object(
|
||||
Bucket=default_storage.bucket_name,
|
||||
Key=key
|
||||
)
|
||||
pdf_content = pdf_response['Body'].read()
|
||||
|
||||
api_url = f"{settings.ACCESSIBILITY_API_BASE_URL}/transcribe/pdf"
|
||||
files = {'file': ('document.pdf', pdf_content, 'application/pdf')}
|
||||
headers = {'Accept': 'application/json'}
|
||||
response = requests.post(api_url, files=files, headers=headers)
|
||||
response.raise_for_status()
|
||||
|
||||
transcribed_text = response.json()['markdown_content']
|
||||
return transcribed_text
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to transcribe PDF: {str(e)}")
|
||||
|
||||
@@ -458,6 +458,10 @@ def test_api_document_invitations_create_email_from_content_language():
|
||||
|
||||
email_content = " ".join(email.body.split())
|
||||
assert f"{user.full_name} a partagé un document avec vous!" in email_content
|
||||
assert (
|
||||
"Docs, votre nouvel outil incontournable pour organiser, partager et collaborer "
|
||||
"sur vos documents en équipe." in email_content
|
||||
)
|
||||
|
||||
|
||||
def test_api_document_invitations_create_email_from_content_language_not_supported():
|
||||
|
||||
@@ -517,6 +517,12 @@ class Base(Configuration):
|
||||
AI_BASE_URL = values.Value(None, environ_name="AI_BASE_URL", environ_prefix=None)
|
||||
AI_MODEL = values.Value(None, environ_name="AI_MODEL", environ_prefix=None)
|
||||
|
||||
ACCESSIBILITY_API_BASE_URL = values.Value(
|
||||
None,
|
||||
environ_name="ACCESSIBILITY_API_BASE_URL",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
AI_DOCUMENT_RATE_THROTTLE_RATES = {
|
||||
"minute": 5,
|
||||
"hour": 100,
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-01-29 13:43+0000\n"
|
||||
"PO-Revision-Date: 2025-01-30 10:24\n"
|
||||
"POT-Creation-Date: 2025-02-06 15:30+0000\n"
|
||||
"PO-Revision-Date: 2025-02-10 14:14\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -391,3 +391,24 @@ msgstr "Französisch"
|
||||
msgid "German"
|
||||
msgstr "Deutsch"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo-E-Mail"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Öffnen"
|
||||
|
||||
#: 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. "
|
||||
|
||||
#: 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 "
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-01-29 13:43+0000\n"
|
||||
"PO-Revision-Date: 2025-01-30 10:24\n"
|
||||
"POT-Creation-Date: 2025-02-06 15:30+0000\n"
|
||||
"PO-Revision-Date: 2025-02-10 14:14\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_US\n"
|
||||
@@ -391,3 +391,24 @@ msgstr ""
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
msgid "Open"
|
||||
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 ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-01-29 13:43+0000\n"
|
||||
"PO-Revision-Date: 2025-01-30 10:24\n"
|
||||
"POT-Creation-Date: 2025-02-06 15:30+0000\n"
|
||||
"PO-Revision-Date: 2025-02-10 14:14\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr_FR\n"
|
||||
@@ -391,3 +391,24 @@ msgstr ""
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo de l'e-mail"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Ouvrir"
|
||||
|
||||
#: 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, votre nouvel outil incontournable pour organiser, partager et collaborer sur vos documents en équipe. "
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr " Proposé par %(brandname)s "
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-01-29 13:43+0000\n"
|
||||
"PO-Revision-Date: 2025-01-30 10:24\n"
|
||||
"POT-Creation-Date: 2025-02-06 15:30+0000\n"
|
||||
"PO-Revision-Date: 2025-02-10 14:14\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Language: nl_NL\n"
|
||||
@@ -391,3 +391,24 @@ msgstr ""
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
msgid "Open"
|
||||
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 ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export const keyCloakSignIn = async (page: Page, browserName: string) => {
|
||||
export const keyCloakSignIn = async (
|
||||
page: Page,
|
||||
browserName: string,
|
||||
fromHome: boolean = true,
|
||||
) => {
|
||||
if (fromHome) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Proconnect Login' })
|
||||
.first()
|
||||
.click();
|
||||
}
|
||||
|
||||
const login = `user-e2e-${browserName}`;
|
||||
const password = `password-e2e-${browserName}`;
|
||||
|
||||
@@ -258,3 +269,8 @@ export const mockedAccesses = async (page: Page, json?: object) => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const expectLoginPage = async (page: Page) =>
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Collaborative writing' }),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -63,27 +63,6 @@ test.describe('Config', () => {
|
||||
expect((await consoleMessage).text()).toContain(invalidMsg);
|
||||
});
|
||||
|
||||
test('it checks that theme is configured from config endpoint', async ({
|
||||
page,
|
||||
}) => {
|
||||
const responsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/config/') && response.status() === 200,
|
||||
);
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const response = await responsePromise;
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
const jsonResponse = await response.json();
|
||||
expect(jsonResponse.FRONTEND_THEME).toStrictEqual('dsfr');
|
||||
|
||||
const footer = page.locator('footer').first();
|
||||
// alt 'Gouvernement Logo' comes from the theme
|
||||
await expect(footer.getByAltText('Gouvernement Logo')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it checks that media server is configured from config endpoint', async ({
|
||||
page,
|
||||
browserName,
|
||||
@@ -161,3 +140,28 @@ test.describe('Config', () => {
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Config: Not loggued', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('it checks that theme is configured from config endpoint', async ({
|
||||
page,
|
||||
}) => {
|
||||
const responsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/config/') && response.status() === 200,
|
||||
);
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const response = await responsePromise;
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
const jsonResponse = await response.json();
|
||||
expect(jsonResponse.FRONTEND_THEME).toStrictEqual('dsfr');
|
||||
|
||||
const footer = page.locator('footer').first();
|
||||
// alt 'Gouvernement Logo' comes from the theme
|
||||
await expect(footer.getByAltText('Gouvernement Logo')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,8 +24,6 @@ test.describe('Doc Create', () => {
|
||||
const header = page.locator('header').first();
|
||||
await header.locator('h2').getByText('Docs').click();
|
||||
|
||||
await expect(page.getByTestId('grid-loader')).toBeVisible();
|
||||
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid).toBeVisible();
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
|
||||
@@ -368,84 +368,4 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it checks the divider block', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'divider-block', browserName, 1);
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
// Trigger slash menu to show menu
|
||||
await editor.click();
|
||||
await editor.fill('/');
|
||||
await page.getByText('Divider', { exact: true }).click();
|
||||
|
||||
await expect(
|
||||
editor.locator('.bn-block-content[data-content-type="divider"]'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('it checks the quote block', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'divider-block', browserName, 1);
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
// Trigger slash menu to show menu
|
||||
await editor.click();
|
||||
await editor.fill('/');
|
||||
await page.getByText('Quote', { exact: true }).click();
|
||||
|
||||
await expect(
|
||||
editor.locator('.bn-block-content[data-content-type="quote"]'),
|
||||
).toBeVisible();
|
||||
|
||||
await editor.fill('Hello World');
|
||||
|
||||
await expect(editor.getByText('Hello World')).toHaveCSS(
|
||||
'font-style',
|
||||
'italic',
|
||||
);
|
||||
});
|
||||
|
||||
test('it checks the alert block', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'divider-block', browserName, 1);
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
// Trigger slash menu to show menu
|
||||
await editor.click();
|
||||
await editor.fill('/');
|
||||
await page.getByText('Alert', { exact: true }).click();
|
||||
|
||||
const alertBlock = editor.locator(
|
||||
'.bn-block-content[data-content-type="alert"]',
|
||||
);
|
||||
await expect(
|
||||
alertBlock.locator('div[data-alert-type="warning"]'),
|
||||
).toBeVisible();
|
||||
await editor.fill('My alert');
|
||||
await expect(alertBlock.getByText('My alert')).toBeVisible();
|
||||
|
||||
await alertBlock.getByText('warning').click();
|
||||
|
||||
await expect(
|
||||
alertBlock.getByRole('menuitem', { name: 'warning Warning' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
alertBlock.getByRole('menuitem', { name: 'error Error' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
alertBlock.getByRole('menuitem', { name: 'info Info' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
alertBlock.getByRole('menuitem', { name: 'check_circle Success' }),
|
||||
).toBeVisible();
|
||||
|
||||
await alertBlock
|
||||
.getByRole('menuitem', { name: 'check_circle Success' })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
alertBlock.locator('div[data-alert-type="success"]'),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,8 +41,16 @@ test.describe('Doc Export', () => {
|
||||
await expect(page.getByRole('button', { name: 'Download' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('it exports the doc to pdf', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
|
||||
test('it exports the doc with pdf line break', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [randomDoc] = await createDoc(
|
||||
page,
|
||||
'doc-editor-line-break',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
@@ -50,8 +58,20 @@ test.describe('Doc Export', () => {
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
const editor = page.locator('.ProseMirror.bn-editor');
|
||||
|
||||
await editor.click();
|
||||
await editor.locator('.bn-block-outer').last().fill('Hello');
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
await editor.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Page Break').click();
|
||||
|
||||
await expect(editor.locator('.bn-page-break')).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await editor.locator('.bn-block-outer').last().fill('World');
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
@@ -69,9 +89,10 @@ test.describe('Doc Export', () => {
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfText = (await pdf(pdfBuffer)).text;
|
||||
const pdfData = await pdf(pdfBuffer);
|
||||
|
||||
expect(pdfText).toContain('Hello World'); // This is the doc text
|
||||
expect(pdfData.numpages).toBe(2);
|
||||
expect(pdfData.text).toContain('\n\nHello\n\nWorld'); // This is the doc text
|
||||
});
|
||||
|
||||
test('it exports the doc to docx', async ({ page, browserName }) => {
|
||||
|
||||
@@ -213,7 +213,6 @@ test.describe('Document grid item options', () => {
|
||||
test.describe('Documents filters', () => {
|
||||
test('it checks the prebuild left panel filters', async ({ page }) => {
|
||||
// All Docs
|
||||
await expect(page.getByTestId('grid-loader')).toBeVisible();
|
||||
const response = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().endsWith('documents/?page=1') &&
|
||||
@@ -254,7 +253,6 @@ test.describe('Documents filters', () => {
|
||||
url = new URL(page.url());
|
||||
target = url.searchParams.get('target');
|
||||
expect(target).toBe('my_docs');
|
||||
await expect(page.getByTestId('grid-loader')).toBeVisible();
|
||||
const responseMyDocs = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().endsWith('documents/?page=1&is_creator_me=true') &&
|
||||
@@ -270,7 +268,6 @@ test.describe('Documents filters', () => {
|
||||
url = new URL(page.url());
|
||||
target = url.searchParams.get('target');
|
||||
expect(target).toBe('shared_with_me');
|
||||
await expect(page.getByTestId('grid-loader')).toBeVisible();
|
||||
const responseSharedWithMe = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('documents/?page=1&is_creator_me=false') &&
|
||||
@@ -291,8 +288,6 @@ test.describe('Documents Grid', () => {
|
||||
|
||||
test('checks all the elements are visible', async ({ page }) => {
|
||||
let docs: SmallDoc[] = [];
|
||||
await expect(page.getByTestId('grid-loader')).toBeVisible();
|
||||
|
||||
const response = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().endsWith('documents/?page=1') &&
|
||||
|
||||
@@ -395,9 +395,7 @@ test.describe('Doc Header', () => {
|
||||
navigator.clipboard.readText(),
|
||||
);
|
||||
const clipboardContent = await handle.jsonValue();
|
||||
expect(clipboardContent.trim()).toBe(
|
||||
`<h1 data-level=\"1\">Hello World</h1><p></p>`,
|
||||
);
|
||||
expect(clipboardContent.trim()).toBe(`<h1>Hello World</h1><p></p>`);
|
||||
});
|
||||
|
||||
test('it checks the copy link button', async ({ page }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { keyCloakSignIn, mockedDocument } from './common';
|
||||
import { expectLoginPage, keyCloakSignIn, mockedDocument } from './common';
|
||||
|
||||
test.describe('Doc Routing', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -63,16 +63,13 @@ test.describe('Doc Routing: Not loggued', () => {
|
||||
await page.goto('/docs/mocked-document-id/');
|
||||
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
await keyCloakSignIn(page, browserName);
|
||||
await keyCloakSignIn(page, browserName, false);
|
||||
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line playwright/expect-expect
|
||||
test('The homepage redirects to login.', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Sign In',
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expectLoginPage(page);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, keyCloakSignIn, verifyDocName } from './common';
|
||||
import {
|
||||
createDoc,
|
||||
expectLoginPage,
|
||||
keyCloakSignIn,
|
||||
verifyDocName,
|
||||
} from './common';
|
||||
|
||||
const browsersName = ['chromium', 'webkit', 'firefox'];
|
||||
|
||||
@@ -91,7 +96,7 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
|
||||
await expectLoginPage(page);
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
@@ -121,6 +126,10 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
|
||||
await keyCloakSignIn(page, otherBrowser!);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
await expect(
|
||||
@@ -169,10 +178,11 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
|
||||
await keyCloakSignIn(page, otherBrowser!);
|
||||
|
||||
await page.goto(urlDoc);
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
|
||||
).toBeVisible();
|
||||
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
await page.goto(urlDoc);
|
||||
|
||||
await verifyDocName(page, docTitle);
|
||||
await expect(page.getByLabel('Share button')).toBeVisible();
|
||||
@@ -247,7 +257,7 @@ test.describe('Doc Visibility: Public', () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
|
||||
await expectLoginPage(page);
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
@@ -313,7 +323,7 @@ test.describe('Doc Visibility: Public', () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
|
||||
await expectLoginPage(page);
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
@@ -364,7 +374,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
|
||||
await expectLoginPage(page);
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
@@ -414,6 +424,10 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
const otherBrowser = browsersName.find((b) => b !== browserName);
|
||||
await keyCloakSignIn(page, otherBrowser!);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||
@@ -470,6 +484,10 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
const otherBrowser = browsersName.find((b) => b !== browserName);
|
||||
await keyCloakSignIn(page, otherBrowser!);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { goToGridDoc } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Footer', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('checks all the elements are visible', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const footer = page.locator('footer').first();
|
||||
|
||||
await expect(footer.getByAltText('Gouvernement Logo')).toBeVisible();
|
||||
@@ -47,12 +44,6 @@ test.describe('Footer', () => {
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks footer is not visible on doc editor', async ({ page }) => {
|
||||
await expect(page.locator('footer')).toBeVisible();
|
||||
await goToGridDoc(page);
|
||||
await expect(page.locator('footer')).toBeHidden();
|
||||
});
|
||||
|
||||
const legalPages = [
|
||||
{ name: 'Legal Notice', url: '/legal-notice/' },
|
||||
{ name: 'Personal data and cookies', url: '/personal-data-cookies/' },
|
||||
@@ -60,6 +51,8 @@ test.describe('Footer', () => {
|
||||
];
|
||||
for (const { name, url } of legalPages) {
|
||||
test(`checks ${name} page`, async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
const footer = page.locator('footer').first();
|
||||
await footer.getByRole('link', { name }).click();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { keyCloakSignIn } from './common';
|
||||
import { expectLoginPage, keyCloakSignIn } from './common';
|
||||
|
||||
test.describe('Header', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -98,6 +98,6 @@ test.describe('Header: Log out', () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
|
||||
await expectLoginPage(page);
|
||||
});
|
||||
});
|
||||
|
||||
52
src/frontend/apps/e2e/__tests__/app-impress/home.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/docs/');
|
||||
});
|
||||
|
||||
test.describe('Home page', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
test('checks all the elements are visible', async ({ page }) => {
|
||||
// Check header content
|
||||
const header = page.locator('header').first();
|
||||
const footer = page.locator('footer').first();
|
||||
await expect(header).toBeVisible();
|
||||
await expect(
|
||||
header.getByRole('combobox', { name: 'Language' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
header.getByRole('button', { name: 'Les services de La Suite numé' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
header.getByRole('img', { name: 'Gouvernement Logo' }),
|
||||
).toBeVisible();
|
||||
await expect(header.getByRole('img', { name: 'Docs logo' })).toBeVisible();
|
||||
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
|
||||
await expect(header.getByText('BETA')).toBeVisible();
|
||||
|
||||
// Check the titles
|
||||
const h2 = page.locator('h2');
|
||||
await expect(
|
||||
h2.getByText('Collaborative writing, Simplified.'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
h2.getByText('An uncompromising writing experience.'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
h2.getByText('Simple and secure collaboration.'),
|
||||
).toBeVisible();
|
||||
await expect(h2.getByText('Flexible export.')).toBeVisible();
|
||||
await expect(
|
||||
h2.getByText('A new way to organize knowledge.'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByText('Docs is already available, log in to use it now.'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Proconnect Login' }),
|
||||
).toHaveCount(2);
|
||||
|
||||
await expect(footer).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@
|
||||
"test:ui::chromium": "yarn test:ui --project=chromium"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.50.0",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@types/node": "*",
|
||||
"@types/pdf-parse": "1.1.4",
|
||||
"eslint-config-impress": "*",
|
||||
|
||||
@@ -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: {
|
||||
@@ -247,6 +253,9 @@ const config = {
|
||||
'la-gauffre': {
|
||||
activated: false,
|
||||
},
|
||||
'home-proconnect': {
|
||||
activated: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
dsfr: {
|
||||
@@ -379,8 +388,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)',
|
||||
},
|
||||
@@ -462,6 +471,9 @@ const config = {
|
||||
'la-gauffre': {
|
||||
activated: true,
|
||||
},
|
||||
'home-proconnect': {
|
||||
activated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -15,34 +15,35 @@
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocknote/core": "0.21.0",
|
||||
"@blocknote/mantine": "0.21.0",
|
||||
"@blocknote/react": "0.21.0",
|
||||
"@blocknote/xl-docx-exporter": "0.21.0",
|
||||
"@blocknote/xl-pdf-exporter": "0.21.0",
|
||||
"@blocknote/core": "0.23.2",
|
||||
"@blocknote/mantine": "0.23.2",
|
||||
"@blocknote/react": "0.23.2",
|
||||
"@blocknote/xl-docx-exporter": "0.23.2",
|
||||
"@blocknote/xl-pdf-exporter": "0.23.2",
|
||||
"@gouvfr-lasuite/integration": "1.0.2",
|
||||
"@hocuspocus/provider": "2.15.1",
|
||||
"@hocuspocus/provider": "2.15.2",
|
||||
"@openfun/cunningham-react": "2.9.4",
|
||||
"@react-pdf/renderer": "4.1.6",
|
||||
"@sentry/nextjs": "8.52.0",
|
||||
"@tanstack/react-query": "5.65.1",
|
||||
"@sentry/nextjs": "8.54.0",
|
||||
"@tanstack/react-query": "5.66.0",
|
||||
"cmdk": "1.0.4",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"docx": "9.1.1",
|
||||
"i18next": "24.2.2",
|
||||
"i18next-browser-languagedetector": "8.0.2",
|
||||
"idb": "8.0.1",
|
||||
"idb": "8.0.2",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.5.0",
|
||||
"marked": "^15.0.7",
|
||||
"next": "15.1.6",
|
||||
"posthog-js": "1.211.3",
|
||||
"posthog-js": "1.215.6",
|
||||
"react": "*",
|
||||
"react-aria-components": "1.6.0",
|
||||
"react-dom": "*",
|
||||
"react-i18next": "15.4.0",
|
||||
"react-intersection-observer": "9.15.1",
|
||||
"react-select": "5.10.0",
|
||||
"styled-components": "6.1.14",
|
||||
"styled-components": "6.1.15",
|
||||
"use-debounce": "10.0.4",
|
||||
"y-protocols": "1.0.6",
|
||||
"yjs": "13.6.23",
|
||||
@@ -50,7 +51,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@tanstack/react-query-devtools": "5.65.1",
|
||||
"@tanstack/react-query-devtools": "5.66.0",
|
||||
"@testing-library/dom": "10.4.0",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "16.2.0",
|
||||
|
||||
BIN
src/frontend/apps/impress/public/assets/SC1-en.webm
Normal file
BIN
src/frontend/apps/impress/public/assets/SC1-fr.webm
Normal file
BIN
src/frontend/apps/impress/public/assets/ia_baguette.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 5.3 KiB |
18
src/frontend/apps/impress/src/assets/icons/icon-docs.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg
|
||||
width="32"
|
||||
height="33"
|
||||
viewBox="0 0 32 33"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21.6305 29.5812C22.7983 29.2538 23.9166 28.6562 24.6505 27.6003C25.3749 26.5663 25.5789 25.2547 25.5789 23.9925V5.50099C25.5789 5.17358 25.5611 4.84557 25.5216 4.52148C26.1016 4.74961 26.5486 5.12658 26.8626 5.65239C27.2331 6.25024 27.4184 7.03757 27.4184 8.01435V26.7964C27.4184 28.1184 27.0942 29.1078 26.4458 29.7646C25.7974 30.4214 24.8207 30.7498 23.5155 30.7498H16.4209C16.5889 30.7204 16.7574 30.6901 16.9262 30.659C18.4067 30.3944 19.9713 30.0354 21.6185 29.5846L21.6305 29.5812Z"
|
||||
fill="#C9191E"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.58203 26.405V7.5977C4.58203 6.45251 4.88938 5.58519 5.50408 4.99575C6.1272 4.40631 6.95242 4.08212 7.97972 4.02318C9.49542 3.93055 10.9311 3.80425 12.2868 3.64425C13.6425 3.47584 14.9393 3.28217 16.1771 3.06324C17.4234 2.8443 18.6359 2.60011 19.8148 2.33065C21.0274 2.04435 21.9578 2.1875 22.6062 2.7601C23.2546 3.33269 23.5788 4.24632 23.5788 5.50099V23.9925C23.5788 25.0956 23.3893 25.9166 23.0104 26.4555C22.6315 27.0029 21.9915 27.4028 21.0905 27.6554C19.4906 28.0933 17.9833 28.4386 16.5687 28.6912C15.154 28.9522 13.7731 29.1501 12.4258 29.2848C11.0785 29.4196 9.69751 29.5248 8.28286 29.6006C7.11241 29.668 6.20299 29.4238 5.5546 28.868C4.90622 28.3207 4.58203 27.4997 4.58203 26.405ZM9.20865 11.0124C11.0635 10.8944 12.7632 10.7131 14.3075 10.4683C14.6822 10.4072 15.0564 10.3436 15.4291 10.2776C15.8192 10.2085 16.1013 9.86859 16.1013 9.47337C16.1013 8.96154 15.638 8.57609 15.135 8.66189C14.846 8.71118 14.5555 8.75909 14.2635 8.80562C12.7346 9.04923 11.0452 9.22998 9.19523 9.3477C8.91819 9.36558 8.69776 9.45188 8.55608 9.62391C8.42209 9.78661 8.35645 9.98229 8.35645 10.2053C8.35645 10.4321 8.43296 10.6295 8.58568 10.7918L8.58783 10.7939C8.75336 10.9595 8.96369 11.0311 9.20865 11.0124ZM9.20801 15.206C11.0631 15.088 12.763 14.9066 14.3075 14.6619C15.8588 14.4089 17.3936 14.1138 18.9112 13.7766C19.2191 13.7081 19.4498 13.6003 19.5652 13.433C19.6786 13.2721 19.7347 13.0876 19.7347 12.8832C19.7347 12.6526 19.6469 12.454 19.476 12.2926C19.2921 12.1189 19.0348 12.0784 18.7304 12.1411L18.7285 12.1415C17.2823 12.4694 15.794 12.7553 14.2635 12.9992C12.7346 13.2428 11.0452 13.4235 9.19523 13.5413C8.91819 13.5591 8.69776 13.6454 8.55608 13.8175C8.42276 13.9794 8.35645 14.1705 8.35645 14.3863C8.35645 14.6203 8.43209 14.8223 8.58558 14.9854L8.59 14.9896C8.75499 15.1449 8.96316 15.2155 9.20551 15.2062L9.20801 15.206ZM9.20847 19.3994C11.0634 19.2729 12.7631 19.0874 14.3075 18.8427C15.8589 18.5982 17.3934 18.3073 18.9112 17.97C19.2199 17.9014 19.4508 17.7891 19.566 17.6127C19.6783 17.4529 19.7347 17.2733 19.7347 17.0766C19.7347 16.8461 19.6469 16.6474 19.476 16.4861C19.2921 16.3123 19.0348 16.2718 18.7304 16.3345L18.729 16.3348C17.2827 16.6543 15.7942 16.9361 14.2635 17.18C12.7345 17.4236 11.045 17.6086 9.19495 17.7347C8.91804 17.7526 8.69771 17.8389 8.55608 18.0109C8.42276 18.1728 8.35645 18.3639 8.35645 18.5797C8.35645 18.8137 8.43209 19.0158 8.58558 19.1789L8.59 19.183C8.75499 19.3383 8.96316 19.4089 9.20551 19.3996L9.20847 19.3994ZM14.3075 23.007C12.7632 23.2518 11.0635 23.4331 9.20867 23.5512C8.9637 23.5698 8.75337 23.4982 8.58783 23.3326L8.58572 23.3305C8.433 23.1682 8.35645 22.9708 8.35645 22.7441C8.35645 22.521 8.42209 22.3253 8.55608 22.1626C8.69776 21.9906 8.91827 21.9043 9.19531 21.8864C11.0453 21.7687 12.7346 21.588 14.2635 21.3443C14.5555 21.2978 14.846 21.2499 15.135 21.2006C15.638 21.1148 16.1013 21.5003 16.1013 22.0121C16.1013 22.4073 15.8192 22.7472 15.4291 22.8163C15.0564 22.8823 14.6822 22.9459 14.3075 23.007Z"
|
||||
fill="#000091"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -3,10 +3,10 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Auth } from '@/features/auth';
|
||||
import '@/i18n/initI18n';
|
||||
import { useResponsiveStore } from '@/stores/';
|
||||
|
||||
import { Auth } from './auth/';
|
||||
import { ConfigProvider } from './config/';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { PropsWithChildren, useEffect, useState } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
|
||||
import { useAuthStore } from './useAuthStore';
|
||||
|
||||
/**
|
||||
* TODO: Remove this restriction when we will have a homepage design for non-authenticated users.
|
||||
*
|
||||
* We define the paths that are not allowed without authentication.
|
||||
* Actually, only the home page and the docs page are not allowed without authentication.
|
||||
* When we will have a homepage design for non-authenticated users, we will remove this restriction to have
|
||||
* the full website accessible without authentication.
|
||||
*/
|
||||
const regexpUrlsAuth = [/\/docs\/$/g, /^\/$/g];
|
||||
|
||||
export const Auth = ({ children }: PropsWithChildren) => {
|
||||
const { initAuth, initiated, authenticated, login, getAuthUrl } =
|
||||
useAuthStore();
|
||||
const { asPath, replace } = useRouter();
|
||||
|
||||
const [pathAllowed, setPathAllowed] = useState<boolean>(
|
||||
!regexpUrlsAuth.some((regexp) => !!asPath.match(regexp)),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
initAuth();
|
||||
}, [initAuth]);
|
||||
|
||||
useEffect(() => {
|
||||
setPathAllowed(!regexpUrlsAuth.some((regexp) => !!asPath.match(regexp)));
|
||||
}, [asPath]);
|
||||
|
||||
// We force to login except on allowed paths
|
||||
useEffect(() => {
|
||||
if (!initiated || authenticated || pathAllowed) {
|
||||
return;
|
||||
}
|
||||
|
||||
login();
|
||||
}, [authenticated, pathAllowed, login, initiated]);
|
||||
|
||||
// Redirect to the path before login
|
||||
useEffect(() => {
|
||||
if (!authenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
const authUrl = getAuthUrl();
|
||||
if (authUrl) {
|
||||
void replace(authUrl);
|
||||
}
|
||||
}, [authenticated, getAuthUrl, replace]);
|
||||
|
||||
if ((!initiated && pathAllowed) || (!authenticated && !pathAllowed)) {
|
||||
return (
|
||||
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useAuthStore } from '@/core/auth';
|
||||
|
||||
export const ButtonLogin = () => {
|
||||
const { t } = useTranslation();
|
||||
const { logout, authenticated, login } = useAuthStore();
|
||||
|
||||
if (!authenticated) {
|
||||
return (
|
||||
<Button onClick={login} color="primary-text" aria-label={t('Login')}>
|
||||
{t('Login')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={logout} color="primary-text" aria-label={t('Logout')}>
|
||||
{t('Logout')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './getMe';
|
||||
export * from './types';
|
||||
@@ -1 +0,0 @@
|
||||
export const PATH_AUTH_LOCAL_STORAGE = 'docs-path-auth';
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './api/types';
|
||||
export * from './Auth';
|
||||
export * from './ButtonLogin';
|
||||
export * from './useAuthStore';
|
||||
@@ -1,64 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { baseApiUrl } from '@/api';
|
||||
import { terminateCrispSession } from '@/services';
|
||||
|
||||
import { User, getMe } from './api';
|
||||
import { PATH_AUTH_LOCAL_STORAGE } from './conf';
|
||||
|
||||
interface AuthStore {
|
||||
initiated: boolean;
|
||||
authenticated: boolean;
|
||||
initAuth: () => void;
|
||||
logout: () => void;
|
||||
login: () => void;
|
||||
setAuthUrl: (url: string) => void;
|
||||
getAuthUrl: () => string | undefined;
|
||||
userData?: User;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
initiated: false,
|
||||
authenticated: false,
|
||||
userData: undefined,
|
||||
};
|
||||
|
||||
export const useAuthStore = create<AuthStore>((set, get) => ({
|
||||
initiated: initialState.initiated,
|
||||
authenticated: initialState.authenticated,
|
||||
userData: initialState.userData,
|
||||
initAuth: () => {
|
||||
getMe()
|
||||
.then((data: User) => {
|
||||
set({ authenticated: true, userData: data });
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
set({ initiated: true });
|
||||
});
|
||||
},
|
||||
login: () => {
|
||||
get().setAuthUrl(window.location.pathname);
|
||||
|
||||
window.location.replace(`${baseApiUrl()}authenticate/`);
|
||||
},
|
||||
logout: () => {
|
||||
terminateCrispSession();
|
||||
window.location.replace(`${baseApiUrl()}logout/`);
|
||||
},
|
||||
// If we try to access a specific page and we are not authenticated
|
||||
// we store the path in the local storage to redirect to it after login
|
||||
setAuthUrl() {
|
||||
if (window.location.pathname !== '/') {
|
||||
localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.pathname);
|
||||
}
|
||||
},
|
||||
// If a path is stored in the local storage, we return it then remove it
|
||||
getAuthUrl() {
|
||||
const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
|
||||
if (path_auth) {
|
||||
localStorage.removeItem(PATH_AUTH_LOCAL_STORAGE);
|
||||
return path_auth;
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './AppProvider';
|
||||
export * from './auth';
|
||||
export * from './config';
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
--c--theme--colors--danger-text: var(--c--theme--colors--greyscale-000);
|
||||
--c--theme--colors--card-border: #ededed;
|
||||
--c--theme--colors--primary-bg: #fafafa;
|
||||
--c--theme--colors--primary-action: #1212ff;
|
||||
--c--theme--colors--primary-050: #f5f5fe;
|
||||
--c--theme--colors--primary-150: #e5eefa;
|
||||
--c--theme--colors--primary-950: #1b1b35;
|
||||
@@ -122,6 +123,11 @@
|
||||
--c--theme--font--sizes--ml: 0.938rem;
|
||||
--c--theme--font--sizes--xl: 1.25rem;
|
||||
--c--theme--font--sizes--t: 0.6875rem;
|
||||
--c--theme--font--sizes--xl-alt: 5rem;
|
||||
--c--theme--font--sizes--lg-alt: 4.5rem;
|
||||
--c--theme--font--sizes--md-alt: 4rem;
|
||||
--c--theme--font--sizes--sm-alt: 3.5rem;
|
||||
--c--theme--font--sizes--xs-alt: 3rem;
|
||||
--c--theme--font--weights--thin: 100;
|
||||
--c--theme--font--weights--light: 300;
|
||||
--c--theme--font--weights--regular: 400;
|
||||
@@ -316,7 +322,7 @@
|
||||
--c--theme--colors--primary-700
|
||||
);
|
||||
--c--components--button--secondary--border--color: var(
|
||||
--c--theme--colors--primary-200
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--button--tertiary--color: var(
|
||||
--c--theme--colors--primary-text
|
||||
@@ -339,6 +345,7 @@
|
||||
--c--components--button--disabled--color: white;
|
||||
--c--components--button--disabled--background--color: #b3cef0;
|
||||
--c--components--la-gauffre--activated: false;
|
||||
--c--components--home-proconnect--activated: false;
|
||||
}
|
||||
|
||||
.cunningham-theme--dark {
|
||||
@@ -501,10 +508,10 @@
|
||||
--c--components--button--secondary--background--color-hover: #f6f6f6;
|
||||
--c--components--button--secondary--background--color-active: #ededed;
|
||||
--c--components--button--secondary--border--color: var(
|
||||
--c--theme--colors--primary-600
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--button--secondary--border--color-hover: var(
|
||||
--c--theme--colors--primary-600
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--button--secondary--color: var(
|
||||
--c--theme--colors--primary-text
|
||||
@@ -584,6 +591,7 @@
|
||||
);
|
||||
--c--components--forms-textarea--border-radius: 0;
|
||||
--c--components--la-gauffre--activated: true;
|
||||
--c--components--home-proconnect--activated: true;
|
||||
}
|
||||
|
||||
.clr-secondary-text {
|
||||
@@ -874,6 +882,10 @@
|
||||
color: var(--c--theme--colors--primary-bg);
|
||||
}
|
||||
|
||||
.clr-primary-action {
|
||||
color: var(--c--theme--colors--primary-action);
|
||||
}
|
||||
|
||||
.clr-primary-050 {
|
||||
color: var(--c--theme--colors--primary-050);
|
||||
}
|
||||
@@ -1302,6 +1314,10 @@
|
||||
background-color: var(--c--theme--colors--primary-bg);
|
||||
}
|
||||
|
||||
.bg-primary-action {
|
||||
background-color: var(--c--theme--colors--primary-action);
|
||||
}
|
||||
|
||||
.bg-primary-050 {
|
||||
background-color: var(--c--theme--colors--primary-050);
|
||||
}
|
||||
@@ -1550,6 +1566,31 @@
|
||||
letter-spacing: var(--c--theme--font--letterspacings--t);
|
||||
}
|
||||
|
||||
.fs-xl-alt {
|
||||
font-size: var(--c--theme--font--sizes--xl-alt);
|
||||
letter-spacing: var(--c--theme--font--letterspacings--xl-alt);
|
||||
}
|
||||
|
||||
.fs-lg-alt {
|
||||
font-size: var(--c--theme--font--sizes--lg-alt);
|
||||
letter-spacing: var(--c--theme--font--letterspacings--lg-alt);
|
||||
}
|
||||
|
||||
.fs-md-alt {
|
||||
font-size: var(--c--theme--font--sizes--md-alt);
|
||||
letter-spacing: var(--c--theme--font--letterspacings--md-alt);
|
||||
}
|
||||
|
||||
.fs-sm-alt {
|
||||
font-size: var(--c--theme--font--sizes--sm-alt);
|
||||
letter-spacing: var(--c--theme--font--letterspacings--sm-alt);
|
||||
}
|
||||
|
||||
.fs-xs-alt {
|
||||
font-size: var(--c--theme--font--sizes--xs-alt);
|
||||
letter-spacing: var(--c--theme--font--letterspacings--xs-alt);
|
||||
}
|
||||
|
||||
.f-base {
|
||||
font-family: var(--c--theme--font--families--base);
|
||||
}
|
||||
|
||||
@@ -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)',
|
||||
@@ -330,6 +336,7 @@ export const tokens = {
|
||||
disabled: { color: 'white', background: { color: '#b3cef0' } },
|
||||
},
|
||||
'la-gauffre': { activated: false },
|
||||
'home-proconnect': { activated: false },
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
@@ -502,8 +509,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)',
|
||||
},
|
||||
@@ -575,6 +582,7 @@ export const tokens = {
|
||||
},
|
||||
'forms-textarea': { 'border-radius': '0' },
|
||||
'la-gauffre': { activated: true },
|
||||
'home-proconnect': { activated: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Crisp } from 'crisp-sdk-web';
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import { useAuthStore } from '../useAuthStore';
|
||||
import { gotoLogout } from '../utils';
|
||||
|
||||
jest.mock('crisp-sdk-web', () => ({
|
||||
...jest.requireActual('crisp-sdk-web'),
|
||||
@@ -17,7 +17,7 @@ jest.mock('crisp-sdk-web', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useAuthStore', () => {
|
||||
describe('utils', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
fetchMock.restore();
|
||||
@@ -33,7 +33,7 @@ describe('useAuthStore', () => {
|
||||
writable: true,
|
||||
});
|
||||
|
||||
useAuthStore.getState().logout();
|
||||
gotoLogout();
|
||||
|
||||
expect(Crisp.session.reset).toHaveBeenCalled();
|
||||
});
|
||||
2
src/frontend/apps/impress/src/features/auth/api/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './useAuthQuery';
|
||||
export * from './types';
|
||||
@@ -1,4 +1,6 @@
|
||||
import { fetchAPI } from '@/api';
|
||||
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, fetchAPI } from '@/api';
|
||||
|
||||
import { User } from './types';
|
||||
|
||||
@@ -19,3 +21,16 @@ export const getMe = async (): Promise<User> => {
|
||||
}
|
||||
return response.json() as Promise<User>;
|
||||
};
|
||||
|
||||
export const KEY_AUTH = 'auth';
|
||||
|
||||
export function useAuthQuery(
|
||||
queryConfig?: UseQueryOptions<User, APIError, User>,
|
||||
) {
|
||||
return useQuery<User, APIError, User>({
|
||||
queryKey: [KEY_AUTH],
|
||||
queryFn: getMe,
|
||||
staleTime: 1000 * 60 * 15, // 15 minutes
|
||||
...queryConfig,
|
||||
});
|
||||
}
|
||||
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,47 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
|
||||
import { useAuth } from '../hooks';
|
||||
|
||||
export const Auth = ({ children }: PropsWithChildren) => {
|
||||
const { isLoading, pathAllowed, isFetchedAfterMount, authenticated } =
|
||||
useAuth();
|
||||
const { replace, pathname } = useRouter();
|
||||
|
||||
if (isLoading && !isFetchedAfterMount) {
|
||||
return (
|
||||
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user is not authenticated and the path is not allowed, we redirect to the login page.
|
||||
*/
|
||||
if (!authenticated && !pathAllowed) {
|
||||
void replace('/login');
|
||||
return (
|
||||
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user is authenticated and the path is the login page, we redirect to the home page.
|
||||
*/
|
||||
if (pathname === '/login' && authenticated) {
|
||||
void replace('/');
|
||||
return (
|
||||
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { BoxButton } from '@/components';
|
||||
|
||||
import ProConnectImg from '../assets/button-proconnect.svg?url';
|
||||
import { useAuth } from '../hooks';
|
||||
import { gotoLogin, gotoLogout } from '../utils';
|
||||
|
||||
export const ButtonLogin = () => {
|
||||
const { t } = useTranslation();
|
||||
const { authenticated } = useAuth();
|
||||
|
||||
if (!authenticated) {
|
||||
return (
|
||||
<Button onClick={gotoLogin} color="primary-text" aria-label={t('Login')}>
|
||||
{t('Login')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={gotoLogout} color="primary-text" aria-label={t('Logout')}>
|
||||
{t('Logout')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProConnectButton = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<BoxButton
|
||||
onClick={gotoLogin}
|
||||
aria-label={t('Proconnect Login')}
|
||||
$css={css`
|
||||
background-color: var(--c--theme--colors--primary-text);
|
||||
&:hover {
|
||||
background-color: var(--c--theme--colors--primary-action);
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Image src={ProConnectImg} alt={t('ProConnect Image')} />
|
||||
</BoxButton>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Auth';
|
||||
export * from './ButtonLogin';
|
||||
5
src/frontend/apps/impress/src/features/auth/conf.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { baseApiUrl } from '@/api';
|
||||
|
||||
export const PATH_AUTH_LOCAL_STORAGE = 'docs-path-auth';
|
||||
export const LOGIN_URL = `${baseApiUrl()}authenticate/`;
|
||||
export const LOGOUT_URL = `${baseApiUrl()}logout/`;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './useAuth';
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useAuthQuery } from '../api';
|
||||
import { getAuthUrl } from '../utils';
|
||||
|
||||
const regexpUrlsAuth = [/\/docs\/$/g, /\/docs$/g, /^\/$/g];
|
||||
|
||||
export const useAuth = () => {
|
||||
const { data: user, ...authStates } = useAuthQuery();
|
||||
const { pathname, replace } = useRouter();
|
||||
|
||||
const [pathAllowed, setPathAllowed] = useState<boolean>(
|
||||
!regexpUrlsAuth.some((regexp) => !!pathname.match(regexp)),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setPathAllowed(!regexpUrlsAuth.some((regexp) => !!pathname.match(regexp)));
|
||||
}, [pathname]);
|
||||
|
||||
// Redirect to the path before login
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const authUrl = getAuthUrl();
|
||||
if (authUrl) {
|
||||
void replace(authUrl);
|
||||
}
|
||||
}, [user, replace]);
|
||||
|
||||
return { user, authenticated: !!user, pathAllowed, ...authStates };
|
||||
};
|
||||
4
src/frontend/apps/impress/src/features/auth/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './api/types';
|
||||
export * from './components';
|
||||
export * from './hooks';
|
||||
export * from './utils';
|
||||
27
src/frontend/apps/impress/src/features/auth/utils.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { terminateCrispSession } from '@/services/Crisp';
|
||||
|
||||
import { LOGIN_URL, LOGOUT_URL, PATH_AUTH_LOCAL_STORAGE } from './conf';
|
||||
|
||||
export const getAuthUrl = () => {
|
||||
const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
|
||||
if (path_auth) {
|
||||
localStorage.removeItem(PATH_AUTH_LOCAL_STORAGE);
|
||||
return path_auth;
|
||||
}
|
||||
};
|
||||
|
||||
export const setAuthUrl = () => {
|
||||
if (window.location.pathname !== '/') {
|
||||
localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.pathname);
|
||||
}
|
||||
};
|
||||
|
||||
export const gotoLogin = () => {
|
||||
setAuthUrl();
|
||||
window.location.replace(LOGIN_URL);
|
||||
};
|
||||
|
||||
export const gotoLogout = () => {
|
||||
terminateCrispSession();
|
||||
window.location.replace(LOGOUT_URL);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
export type DocAIPdfTranscribe = {
|
||||
docId: string;
|
||||
pdfUrl: string;
|
||||
};
|
||||
|
||||
export type DocAIPdfTranscribeResponse = {
|
||||
document_id: string;
|
||||
};
|
||||
|
||||
export const docAIPdfTranscribe = async ({
|
||||
docId,
|
||||
...params
|
||||
}: DocAIPdfTranscribe): Promise<DocAIPdfTranscribeResponse> => {
|
||||
const response = await fetchAPI(`documents/${docId}/ai-pdf-transcribe/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
...params,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
'Failed to request pdf transcription',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
|
||||
return response.json() as Promise<DocAIPdfTranscribeResponse>;
|
||||
};
|
||||
|
||||
export function useDocAIPdfTranscribe() {
|
||||
return useMutation<DocAIPdfTranscribeResponse, APIError, DocAIPdfTranscribe>({
|
||||
mutationFn: docAIPdfTranscribe,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
useBlockNoteEditor,
|
||||
useComponentsContext,
|
||||
useSelectedBlocks,
|
||||
} from '@blocknote/react';
|
||||
import {
|
||||
Loader,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Text } from '@/components';
|
||||
import { useDocStore } from '@/features/docs/doc-management/';
|
||||
|
||||
import { useDocAIPdfTranscribe } from '../api/useDocAIPdfTranscribe';
|
||||
|
||||
export const AIPdfButton = () => {
|
||||
const editor = useBlockNoteEditor();
|
||||
const Components = useComponentsContext();
|
||||
const selectedBlocks = useSelectedBlocks(editor);
|
||||
const { t } = useTranslation();
|
||||
const { currentDoc } = useDocStore();
|
||||
const { toast } = useToastProvider();
|
||||
const router = useRouter();
|
||||
const { mutateAsync: requestAIPdf, isPending } = useDocAIPdfTranscribe();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
if (!Components || !currentDoc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const show = selectedBlocks.length === 1 && selectedBlocks[0].type === 'file';
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handlePdfTranscription = async () => {
|
||||
console.log('selectedBlocks', selectedBlocks);
|
||||
const pdfBlock = selectedBlocks[0];
|
||||
const props = pdfBlock.props as { url?: string };
|
||||
const pdfUrl = props?.url;
|
||||
console.log('pdfUrl', pdfUrl);
|
||||
if (!props || !pdfUrl) {
|
||||
toast(t('No PDF file found'), VariantType.ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await requestAIPdf({
|
||||
docId: currentDoc.id,
|
||||
pdfUrl,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
// router.push causes the following error:
|
||||
// TypeError: Cannot read properties of undefined (reading 'isDestroyed')
|
||||
// void router.push(`/docs/${response.document_id}`);
|
||||
window.location.href = `/docs/${response.document_id}?albert=true`;
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('error', error);
|
||||
toast(t('Failed to transcribe PDF'), VariantType.ERROR);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Components.FormattingToolbar.Button
|
||||
className="bn-button bn-menu-item"
|
||||
data-test="ai-pdf-transcribe"
|
||||
label="AI"
|
||||
mainTooltip={t('Transcribe PDF')}
|
||||
icon={
|
||||
isLoading ? (
|
||||
<Loader size="small" />
|
||||
) : (
|
||||
<Text $isMaterialIcon $size="l">
|
||||
auto_awesome
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
onClick={() => void handlePdfTranscription()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,303 @@
|
||||
import { Button, Input, Loader } from '@openfun/cunningham-react';
|
||||
import { marked } from 'marked';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { fetchAPI } from '@/api';
|
||||
import { Box, Text } from '@/components';
|
||||
import { Doc } from '@/features/docs';
|
||||
|
||||
import { useEditorStore } from '../../stores/useEditorStore';
|
||||
|
||||
export const AIButtonEl = styled.button`
|
||||
background-image: url('/assets/ia_baguette.png');
|
||||
background-size: cover;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
`;
|
||||
|
||||
export const SuggestionButton = styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
color: var(--c--theme--colors--greyscale-900);
|
||||
height: 32px;
|
||||
padding: 0 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
}
|
||||
|
||||
span.material-icons {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
span.sub {
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
margin-left: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AiButton = ({ doc }: { doc: Doc }) => {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(searchParams.get('albert') === 'true');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
$position="absolute"
|
||||
$css={`
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 1rem;
|
||||
margin: 1rem;
|
||||
z-index: 1;
|
||||
|
||||
`}
|
||||
>
|
||||
<AIButtonEl
|
||||
aria-label="Posez une question à Albert à propos de ce document"
|
||||
onClick={() => setIsOpen(true)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<AiChat doc={doc} isOpen={isOpen} onClose={() => setIsOpen(false)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
type Message = {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
};
|
||||
|
||||
const AiChat = (props: { isOpen: boolean; onClose: () => void; doc: Doc }) => {
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const { editor } = useEditorStore();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
|
||||
if (!props.isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const newPrompt = async (prompt: string) => {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setMessages([...messages, { role: 'user', content: prompt }]);
|
||||
|
||||
const editorContentFormatted = await editor.blocksToMarkdownLossy();
|
||||
|
||||
const response = await fetchAPI(`documents/${props.doc.id}/ai-proxy/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
system:
|
||||
'You are a helpful assistant. You are given a text in markdown format and you need to answer the question. Here is the text: ' +
|
||||
editorContentFormatted,
|
||||
text: prompt,
|
||||
}),
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const data = (await response.json()) as string;
|
||||
|
||||
console.log('response', data);
|
||||
setMessages((messages) => [
|
||||
...messages,
|
||||
{ role: 'assistant', content: data },
|
||||
]);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const submit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPrompt(''); // Clear the prompt after submitting the form
|
||||
await newPrompt(prompt);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
$position="absolute"
|
||||
$css={`
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 1rem;
|
||||
width: 450px;
|
||||
min-height: min(61vh, 365px);
|
||||
box-shadow: rgba(15, 15, 15, 0.04) 0px 0px 0px 1px, rgba(15, 15, 15, 0.03) 0px 3px 6px, rgba(15, 15, 15, 0.06) 0px 9px 24px;
|
||||
max-height: max(-180px + 100vh, 365px);
|
||||
overflow-y: auto;
|
||||
border-radius: 16px;
|
||||
background-color: white;
|
||||
margin: 1rem;
|
||||
z-index: 2;
|
||||
`}
|
||||
$direction="column"
|
||||
>
|
||||
<Box $direction="row" $align="center" $justify="space-between">
|
||||
<Text $theme="greyscale" $variation="1000" $weight="bold" $size="s">
|
||||
{messages.length == 0 ? '' : 'Demander à Albert'}
|
||||
</Text>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={props.onClose}
|
||||
color="tertiary-text"
|
||||
icon={<span className="material-icons">close</span>}
|
||||
/>
|
||||
</Box>
|
||||
{messages.length == 0 && (
|
||||
<Box $gap="1rem" $position="relative" $css="top: -24px;">
|
||||
<Box $gap="0.5rem">
|
||||
<Box
|
||||
$css={`
|
||||
background-image: url('/assets/ia_baguette_question_mark.png');
|
||||
background-size: cover;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
`}
|
||||
></Box>
|
||||
<Text $theme="primary" $variation="800">
|
||||
Bonjour, comment puis-je vous aider ?
|
||||
</Text>
|
||||
</Box>
|
||||
<Box $gap="0.5rem">
|
||||
<Text $theme="greyscale" $variation="1000" $weight="bold" $size="s">
|
||||
Suggestions
|
||||
</Text>
|
||||
<Box>
|
||||
<SuggestionButton
|
||||
onClick={() =>
|
||||
void newPrompt(
|
||||
'Resume ce document sous forme textuelle uniquement',
|
||||
)
|
||||
}
|
||||
>
|
||||
<span className="material-icons">description</span>
|
||||
Résumer <span className="sub">cette page</span>
|
||||
</SuggestionButton>
|
||||
<SuggestionButton
|
||||
onClick={() =>
|
||||
void newPrompt('Quel est le sujet principal de ce document ?')
|
||||
}
|
||||
>
|
||||
<span className="material-icons">help_center</span>
|
||||
Poser des questions <span className="sub">sur cette page</span>
|
||||
</SuggestionButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
$flex={1}
|
||||
$direction="column"
|
||||
$gap="1rem"
|
||||
$css={`
|
||||
overflow-y: auto;
|
||||
font-size: 14px;
|
||||
mask-image: linear-gradient(black calc(100% - 32px), transparent calc(100% - 4px));
|
||||
padding-bottom: 32px;
|
||||
`}
|
||||
aria-live="polite"
|
||||
>
|
||||
{messages.map((message, index) => (
|
||||
<Message key={index} message={message} />
|
||||
))}
|
||||
|
||||
{(isLoading || false) && (
|
||||
<Box $display="flex" $direction="row" $align="center" $gap="0.5rem">
|
||||
<Loader size="small" />
|
||||
Albert réfléchit ...
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<form onSubmit={(e) => void submit(e)} style={{ width: '100%' }}>
|
||||
<Input
|
||||
type="text"
|
||||
label="Posez votre question"
|
||||
name="prompt"
|
||||
fullWidth={true}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
value={prompt} // Ensure the input value is updated with the state
|
||||
rightIcon={<span className="material-icons">send</span>}
|
||||
/>
|
||||
</form>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const Message = ({ message }: { message: Message }) => {
|
||||
return (
|
||||
<Box>
|
||||
<Box $direction="row" $align="center" $gap="0.5rem">
|
||||
{message.role === 'user' ? (
|
||||
<Box
|
||||
aria-hidden={true}
|
||||
$css={`
|
||||
background-color:#417DC4;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
VD
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
aria-hidden={true}
|
||||
$css={`
|
||||
background-image: url('/assets/ia_baguette.png');
|
||||
background-size: cover;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
`}
|
||||
></Box>
|
||||
)}
|
||||
<Text $weight="bold">
|
||||
{message.role === 'user' ? 'Vous' : 'Albert'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
$css={`
|
||||
font-size: 12px;
|
||||
padding-left: 34px;
|
||||
color: var(--c--theme--colors--greyscale-700);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: marked.parse(message.content) as string,
|
||||
}}
|
||||
></Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,8 @@
|
||||
import {
|
||||
BlockNoteEditor as BlockNoteEditorCore,
|
||||
BlockNoteSchema,
|
||||
Dictionary,
|
||||
defaultBlockSpecs,
|
||||
locales,
|
||||
withPageBreak,
|
||||
} from '@blocknote/core';
|
||||
import '@blocknote/core/fonts/inter.css';
|
||||
import { BlockNoteView } from '@blocknote/mantine';
|
||||
@@ -15,7 +14,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { Box, TextErrors } from '@/components';
|
||||
import { useAuthStore } from '@/core/auth';
|
||||
import { useAuth } from '@/features/auth';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
|
||||
import { useUploadFile } from '../hook';
|
||||
@@ -27,22 +26,8 @@ import { randomColor } from '../utils';
|
||||
|
||||
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
|
||||
import { BlockNoteToolbar } from './BlockNoteToolbar';
|
||||
import { AlertBlock, DividerBlock, QuoteBlock } from './custom-blocks';
|
||||
|
||||
export const schema = BlockNoteSchema.create({
|
||||
blockSpecs: {
|
||||
...defaultBlockSpecs,
|
||||
alert: AlertBlock,
|
||||
quote: QuoteBlock,
|
||||
divider: DividerBlock,
|
||||
},
|
||||
});
|
||||
|
||||
export type DocsBlockNoteEditor = BlockNoteEditorCore<
|
||||
typeof schema.blockSchema,
|
||||
typeof schema.inlineContentSchema,
|
||||
typeof schema.styleSchema
|
||||
>;
|
||||
export const blockNoteSchema = withPageBreak(BlockNoteSchema.create());
|
||||
|
||||
interface BlockNoteEditorProps {
|
||||
doc: Doc;
|
||||
@@ -50,7 +35,7 @@ interface BlockNoteEditorProps {
|
||||
}
|
||||
|
||||
export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
const { userData } = useAuthStore();
|
||||
const { user } = useAuth();
|
||||
const { setEditor } = useEditorStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -63,7 +48,8 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
|
||||
const collabName = readOnly
|
||||
? 'Reader'
|
||||
: userData?.full_name || userData?.email || t('Anonymous');
|
||||
: user?.full_name || user?.email || t('Anonymous');
|
||||
const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity';
|
||||
|
||||
const editor = useCreateBlockNote(
|
||||
{
|
||||
@@ -75,34 +61,50 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
color: randomColor(),
|
||||
},
|
||||
/**
|
||||
* We re-use the blocknote code to render the cursor but we:
|
||||
* - fix rendering issue with Firefox
|
||||
* - We don't want to show the cursor when anonymous users
|
||||
* We render the cursor with a custom element to:
|
||||
* - fix rendering issue with the default cursor
|
||||
* - hide the cursor when anonymous users
|
||||
*/
|
||||
renderCursor: (user: { color: string; name: string }) => {
|
||||
const cursor = document.createElement('span');
|
||||
const cursorElement = document.createElement('span');
|
||||
|
||||
if (user.name === 'Reader') {
|
||||
return cursor;
|
||||
return cursorElement;
|
||||
}
|
||||
|
||||
cursor.classList.add('collaboration-cursor__caret');
|
||||
cursor.setAttribute('style', `border-color: ${user.color}`);
|
||||
cursorElement.classList.add('collaboration-cursor-custom__base');
|
||||
const caretElement = document.createElement('span');
|
||||
caretElement.classList.add('collaboration-cursor-custom__caret');
|
||||
caretElement.setAttribute('spellcheck', `false`);
|
||||
caretElement.setAttribute('style', `background-color: ${user.color}`);
|
||||
|
||||
const label = document.createElement('span');
|
||||
if (showCursorLabels === 'always') {
|
||||
cursorElement.setAttribute('data-active', '');
|
||||
}
|
||||
|
||||
label.classList.add('collaboration-cursor__label');
|
||||
label.setAttribute('style', `background-color: ${user.color}`);
|
||||
label.insertBefore(document.createTextNode(user.name), null);
|
||||
const labelElement = document.createElement('span');
|
||||
|
||||
cursor.insertBefore(label, null);
|
||||
labelElement.classList.add('collaboration-cursor-custom__label');
|
||||
labelElement.setAttribute('spellcheck', `false`);
|
||||
labelElement.setAttribute(
|
||||
'style',
|
||||
`background-color: ${user.color};border: 1px solid ${user.color};`,
|
||||
);
|
||||
labelElement.insertBefore(document.createTextNode(user.name), null);
|
||||
|
||||
return cursor;
|
||||
caretElement.insertBefore(labelElement, null);
|
||||
|
||||
cursorElement.insertBefore(document.createTextNode('\u2060'), null); // Non-breaking space
|
||||
cursorElement.insertBefore(caretElement, null);
|
||||
cursorElement.insertBefore(document.createTextNode('\u2060'), null); // Non-breaking space
|
||||
|
||||
return cursorElement;
|
||||
},
|
||||
showCursorLabels: showCursorLabels as 'always' | 'activity',
|
||||
},
|
||||
dictionary: locales[lang as keyof typeof locales] as Dictionary,
|
||||
schema,
|
||||
uploadFile,
|
||||
schema: blockNoteSchema,
|
||||
},
|
||||
[collabName, lang, provider, uploadFile],
|
||||
);
|
||||
@@ -135,12 +137,12 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
<BlockNoteView
|
||||
editor={editor}
|
||||
formattingToolbar={false}
|
||||
editable={!readOnly}
|
||||
slashMenu={false}
|
||||
editable={!readOnly}
|
||||
theme="light"
|
||||
>
|
||||
<BlockNoteSuggestionMenu />
|
||||
<BlockNoteToolbar />
|
||||
<BlockNoteSuggestionMenu />
|
||||
</BlockNoteView>
|
||||
</Box>
|
||||
);
|
||||
@@ -165,7 +167,7 @@ export const BlockNoteEditorVersion = ({
|
||||
},
|
||||
provider: undefined,
|
||||
},
|
||||
schema,
|
||||
schema: blockNoteSchema,
|
||||
},
|
||||
[initialContent],
|
||||
);
|
||||
|
||||
@@ -3,17 +3,15 @@ import '@blocknote/mantine/style.css';
|
||||
import {
|
||||
SuggestionMenuController,
|
||||
getDefaultReactSlashMenuItems,
|
||||
getPageBreakReactSlashMenuItems,
|
||||
useBlockNoteEditor,
|
||||
} from '@blocknote/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { DocsBlockNoteEditor } from './BlockNoteEditor';
|
||||
import { insertAlert, insertDivider, insertQuote } from './custom-blocks';
|
||||
import { DocsBlockNoteEditor } from '../types';
|
||||
|
||||
export const BlockNoteSuggestionMenu = () => {
|
||||
const editor = useBlockNoteEditor() as DocsBlockNoteEditor;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getSlashMenuItems = useMemo(() => {
|
||||
return async (query: string) =>
|
||||
@@ -21,14 +19,12 @@ export const BlockNoteSuggestionMenu = () => {
|
||||
filterSuggestionItems(
|
||||
combineByGroup(
|
||||
getDefaultReactSlashMenuItems(editor),
|
||||
[insertAlert(editor, t)],
|
||||
[insertQuote(editor, t)],
|
||||
[insertDivider(editor, t)],
|
||||
getPageBreakReactSlashMenuItems(editor),
|
||||
),
|
||||
query,
|
||||
),
|
||||
);
|
||||
}, [editor, t]);
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<SuggestionMenuController
|
||||
|
||||
@@ -8,21 +8,27 @@ import {
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { AIGroupButton } from './AIButton';
|
||||
import { AIPdfButton } from './AIPdfButton';
|
||||
import { MarkdownButton } from './MarkdownButton';
|
||||
|
||||
export const BlockNoteToolbar = () => {
|
||||
const formattingToolbar = useCallback(
|
||||
({ blockTypeSelectItems }: FormattingToolbarProps) => (
|
||||
<FormattingToolbar>
|
||||
{getFormattingToolbarItems(blockTypeSelectItems)}
|
||||
({ blockTypeSelectItems }: FormattingToolbarProps) => {
|
||||
console.log('formattingToolbar', blockTypeSelectItems);
|
||||
return (
|
||||
<FormattingToolbar>
|
||||
{getFormattingToolbarItems(blockTypeSelectItems)}
|
||||
|
||||
{/* Extra button to do some AI powered actions */}
|
||||
<AIGroupButton key="AIButton" />
|
||||
{/* Extra button to do some AI powered actions */}
|
||||
<AIGroupButton key="AIButton" />
|
||||
|
||||
{/* Extra button to convert from markdown to json */}
|
||||
<MarkdownButton key="customButton" />
|
||||
</FormattingToolbar>
|
||||
),
|
||||
{/* Extra button to convert from markdown to json */}
|
||||
<MarkdownButton key="customButton" />
|
||||
|
||||
<AIPdfButton />
|
||||
</FormattingToolbar>
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { TableContent } from '@/features/docs/doc-table-content/';
|
||||
import { Versions, useDocVersion } from '@/features/docs/doc-versioning/';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { AiButton } from './Ai/AiButton';
|
||||
import { BlockNoteEditor, BlockNoteEditorVersion } from './BlockNoteEditor';
|
||||
|
||||
interface DocEditorProps {
|
||||
@@ -38,6 +39,7 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<AiButton doc={doc} />
|
||||
{isDesktop && !isVersion && (
|
||||
<Box
|
||||
$position="absolute"
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { defaultProps, insertOrUpdateBlock } from '@blocknote/core';
|
||||
import { createReactBlockSpec } from '@blocknote/react';
|
||||
import { Menu } from '@mantine/core';
|
||||
import { TFunction } from 'i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../BlockNoteEditor';
|
||||
|
||||
// The types of alerts that users can choose from.
|
||||
export const alertTypes = [
|
||||
{
|
||||
title: 'Warning',
|
||||
value: 'warning',
|
||||
icon: 'warning',
|
||||
color: 'warning-500',
|
||||
backgroundColor: 'warning-300',
|
||||
},
|
||||
{
|
||||
title: 'Error',
|
||||
value: 'danger',
|
||||
icon: 'error',
|
||||
color: 'danger-500',
|
||||
backgroundColor: 'danger-300',
|
||||
},
|
||||
{
|
||||
title: 'Info',
|
||||
value: 'info',
|
||||
icon: 'info',
|
||||
color: 'info-500',
|
||||
backgroundColor: 'info-300',
|
||||
},
|
||||
{
|
||||
title: 'Success',
|
||||
value: 'success',
|
||||
icon: 'check_circle',
|
||||
color: 'success-500',
|
||||
backgroundColor: 'success-100',
|
||||
},
|
||||
] as const;
|
||||
|
||||
// The Alert block.
|
||||
export const AlertBlock = createReactBlockSpec(
|
||||
{
|
||||
type: 'alert',
|
||||
propSchema: {
|
||||
textAlignment: defaultProps.textAlignment,
|
||||
textColor: defaultProps.textColor,
|
||||
type: {
|
||||
default: 'warning',
|
||||
values: ['warning', 'danger', 'info', 'success'],
|
||||
},
|
||||
},
|
||||
content: 'inline',
|
||||
},
|
||||
{
|
||||
render: (props) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { t } = useTranslation();
|
||||
let alertType = alertTypes.find(
|
||||
(a) => a.value === props.block.props.type,
|
||||
);
|
||||
|
||||
if (!alertType) {
|
||||
alertType = alertTypes[0];
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="alert"
|
||||
data-alert-type={props.block.props.type}
|
||||
$direction="row"
|
||||
$justify="center"
|
||||
$align="center"
|
||||
$radius="4px"
|
||||
$padding="4px"
|
||||
$background={colorsTokens()[alertType.backgroundColor]}
|
||||
$minHeight="48px"
|
||||
$css={css`
|
||||
flex-grow: 1;
|
||||
`}
|
||||
>
|
||||
<Menu withinPortal={false}>
|
||||
<Menu.Target>
|
||||
<Box
|
||||
className="alert-icon-wrapper"
|
||||
$margin={{ horizontal: '12px' }}
|
||||
$radius="16px"
|
||||
$justify="center"
|
||||
$align="center"
|
||||
$height="24px"
|
||||
$width="24px"
|
||||
contentEditable={false}
|
||||
$css="user-select: none; cursor: pointer;"
|
||||
>
|
||||
<Text
|
||||
$isMaterialIcon
|
||||
$theme={alertType.value}
|
||||
$variation="500"
|
||||
$size="20px"
|
||||
>
|
||||
{alertType.icon}
|
||||
</Text>
|
||||
</Box>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown style={{ zIndex: 9999 }}>
|
||||
<Menu.Label>{t('Alert Type')}</Menu.Label>
|
||||
<Menu.Divider />
|
||||
{alertTypes.map((type) => (
|
||||
<Menu.Item
|
||||
key={type.value}
|
||||
leftSection={
|
||||
<Text
|
||||
$isMaterialIcon
|
||||
$color={colorsTokens()[type.color]}
|
||||
$size="16px"
|
||||
>
|
||||
{type.icon}
|
||||
</Text>
|
||||
}
|
||||
onClick={() =>
|
||||
props.editor.updateBlock(props.block, {
|
||||
type: 'alert',
|
||||
props: { type: type.value },
|
||||
})
|
||||
}
|
||||
>
|
||||
{t(type.title)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
<Box
|
||||
className="inline-content"
|
||||
$css={css`
|
||||
flex-grow: 1;
|
||||
& * {
|
||||
color: ${colorsTokens()[alertType.color]};
|
||||
}
|
||||
`}
|
||||
ref={props.contentRef}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const insertAlert = (
|
||||
editor: DocsBlockNoteEditor,
|
||||
t: TFunction<'translation', undefined>,
|
||||
) => ({
|
||||
title: t('Alert'),
|
||||
onItemClick: () => {
|
||||
insertOrUpdateBlock(editor, {
|
||||
type: 'alert',
|
||||
});
|
||||
},
|
||||
aliases: [
|
||||
'alert',
|
||||
'notification',
|
||||
'emphasize',
|
||||
'warning',
|
||||
'error',
|
||||
'info',
|
||||
'success',
|
||||
],
|
||||
group: t('Others'),
|
||||
icon: (
|
||||
<Text $isMaterialIcon $size="18px">
|
||||
warning
|
||||
</Text>
|
||||
),
|
||||
subtext: t('Add a colored alert box'),
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import { defaultProps, insertOrUpdateBlock } from '@blocknote/core';
|
||||
import { createReactBlockSpec } from '@blocknote/react';
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../BlockNoteEditor';
|
||||
|
||||
export const DividerBlock = createReactBlockSpec(
|
||||
{
|
||||
type: 'divider',
|
||||
propSchema: {
|
||||
textAlignment: defaultProps.textAlignment,
|
||||
textColor: defaultProps.textColor,
|
||||
},
|
||||
content: 'none',
|
||||
},
|
||||
{
|
||||
render: () => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '2px',
|
||||
backgroundColor: colorsTokens()['greyscale-300'],
|
||||
margin: '1rem 0',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const insertDivider = (
|
||||
editor: DocsBlockNoteEditor,
|
||||
t: TFunction<'translation', undefined>,
|
||||
) => ({
|
||||
title: t('Divider'),
|
||||
onItemClick: () => {
|
||||
insertOrUpdateBlock(editor, {
|
||||
type: 'divider',
|
||||
});
|
||||
},
|
||||
aliases: ['divider', 'hr', 'horizontal rule', 'line', 'separator'],
|
||||
group: t('Others'),
|
||||
icon: (
|
||||
<span className="material-icons" style={{ fontSize: '18px' }}>
|
||||
remove
|
||||
</span>
|
||||
),
|
||||
subtext: t('Add a horizontal line'),
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import { defaultProps, insertOrUpdateBlock } from '@blocknote/core';
|
||||
import { createReactBlockSpec } from '@blocknote/react';
|
||||
import { TFunction } from 'i18next';
|
||||
import React from 'react';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../BlockNoteEditor';
|
||||
|
||||
export const QuoteBlock = createReactBlockSpec(
|
||||
{
|
||||
type: 'quote',
|
||||
propSchema: {
|
||||
textAlignment: defaultProps.textAlignment,
|
||||
textColor: defaultProps.textColor,
|
||||
},
|
||||
content: 'inline',
|
||||
},
|
||||
{
|
||||
render: (props) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="inline-content"
|
||||
style={{
|
||||
borderLeft: `4px solid ${colorsTokens()['greyscale-300']}`,
|
||||
margin: '0 0 1rem 0',
|
||||
padding: '0.5rem 1rem',
|
||||
color: colorsTokens()['greyscale-600'],
|
||||
fontStyle: 'italic',
|
||||
flexGrow: 1,
|
||||
}}
|
||||
ref={props.contentRef}
|
||||
/>
|
||||
);
|
||||
},
|
||||
parse: () => {
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const insertQuote = (
|
||||
editor: DocsBlockNoteEditor,
|
||||
t: TFunction<'translation', undefined>,
|
||||
) => ({
|
||||
title: t('Quote'),
|
||||
onItemClick: () => {
|
||||
insertOrUpdateBlock(editor, {
|
||||
type: 'quote',
|
||||
});
|
||||
},
|
||||
aliases: ['quote', 'blockquote', 'citation'],
|
||||
group: t('Others'),
|
||||
icon: (
|
||||
<span className="material-icons" style={{ fontSize: '18px' }}>
|
||||
format_quote
|
||||
</span>
|
||||
),
|
||||
subtext: t('Add a quote block'),
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './AlertBlock';
|
||||
export * from './DividerBlock';
|
||||
export * from './QuoteBlock';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../components/BlockNoteEditor';
|
||||
import { useHeadingStore } from '../stores';
|
||||
import { DocsBlockNoteEditor } from '../types';
|
||||
|
||||
export const useHeadings = (editor: DocsBlockNoteEditor) => {
|
||||
const { setHeadings, resetHeadings } = useHeadingStore();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../components/BlockNoteEditor';
|
||||
import { DocsBlockNoteEditor } from '../types';
|
||||
|
||||
export interface UseEditorstore {
|
||||
editor?: DocsBlockNoteEditor;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../components/BlockNoteEditor';
|
||||
import { HeadingBlock } from '../types';
|
||||
import { DocsBlockNoteEditor, HeadingBlock } from '../types';
|
||||
|
||||
const recursiveTextContent = (content: HeadingBlock['content']): string => {
|
||||
if (!content) {
|
||||
|
||||
@@ -6,15 +6,40 @@ export const cssEditor = (readonly: boolean) => css`
|
||||
& .ProseMirror {
|
||||
height: 100%;
|
||||
|
||||
.bn-side-menu[data-block-type='alert'] {
|
||||
height: 55px;
|
||||
.collaboration-cursor-custom__base {
|
||||
position: relative;
|
||||
}
|
||||
.bn-side-menu[data-block-type='divider'] {
|
||||
height: 40px;
|
||||
.collaboration-cursor-custom__caret {
|
||||
position: absolute;
|
||||
height: 85%;
|
||||
width: 2px;
|
||||
bottom: 4%;
|
||||
left: -1px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='quote'] {
|
||||
height: 46px;
|
||||
.collaboration-cursor-custom__label {
|
||||
color: #0d0d0d;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
top: -17px;
|
||||
padding: 0px 6px;
|
||||
border-radius: 0px;
|
||||
white-space: nowrap;
|
||||
transition: clip-path 0.3s ease-in-out;
|
||||
border-radius: 4px 4px 4px 0;
|
||||
box-shadow: inset -2px 2px 6px #ffffff00;
|
||||
clip-path: polygon(0 85%, 4% 85%, 4% 100%, 0% 100%);
|
||||
}
|
||||
.collaboration-cursor-custom__base[data-active]
|
||||
.collaboration-cursor-custom__label {
|
||||
pointer-events: none;
|
||||
box-shadow: inset -2px 2px 6px #ffffff88;
|
||||
clip-path: polygon(0 0, 100% 0%, 100% 100%, 0% 100%);
|
||||
}
|
||||
|
||||
.bn-side-menu[data-block-type='heading'][data-level='1'] {
|
||||
height: 50px;
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
|
||||
import { blockNoteSchema } from './components/BlockNoteEditor';
|
||||
|
||||
export interface DocAttachment {
|
||||
file: string;
|
||||
}
|
||||
@@ -12,3 +16,9 @@ export type HeadingBlock = {
|
||||
level: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type DocsBlockNoteEditor = BlockNoteEditor<
|
||||
typeof blockNoteSchema.blockSchema,
|
||||
typeof blockNoteSchema.inlineContentSchema,
|
||||
typeof blockNoteSchema.styleSchema
|
||||
>;
|
||||
|
||||
@@ -98,7 +98,61 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
|
||||
const exporter = new PDFExporter(
|
||||
editor.schema,
|
||||
pdfDefaultSchemaMappings,
|
||||
{
|
||||
...pdfDefaultSchemaMappings,
|
||||
blockMapping: {
|
||||
...pdfDefaultSchemaMappings.blockMapping,
|
||||
heading: (block, exporter) => {
|
||||
const PIXELS_PER_POINT = 0.75;
|
||||
const MERGE_RATIO = 7.5;
|
||||
const FONT_SIZE = 16;
|
||||
const fontSizeEM =
|
||||
block.props.level === 1
|
||||
? 2
|
||||
: block.props.level === 2
|
||||
? 1.5
|
||||
: 1.17;
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: fontSizeEM * FONT_SIZE * PIXELS_PER_POINT,
|
||||
fontWeight: 700,
|
||||
marginTop: `${fontSizeEM * MERGE_RATIO}px`,
|
||||
marginBottom: `${fontSizeEM * MERGE_RATIO}px`,
|
||||
}}
|
||||
>
|
||||
{exporter.transformInlineContent(block.content)}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
paragraph: (block, exporter) => {
|
||||
/**
|
||||
* Breakline in the editor are not rendered in the PDF
|
||||
* By adding a space if the block is empty we ensure that the block is rendered
|
||||
*/
|
||||
if (Array.isArray(block.content)) {
|
||||
block.content.forEach((content) => {
|
||||
if (content.type === 'text' && !content.text) {
|
||||
content.text = ' ';
|
||||
}
|
||||
});
|
||||
|
||||
if (!block.content.length) {
|
||||
block.content.push({
|
||||
styles: {},
|
||||
text: ' ',
|
||||
type: 'text',
|
||||
});
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Text key={block.id}>
|
||||
{exporter.transformInlineContent(block.content)}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
resolveFileUrl: async (url) =>
|
||||
exportResolveFileUrl(url, defaultExporter.options.resolveFileUrl),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { User } from '@/core';
|
||||
import { User } from '@/features/auth';
|
||||
|
||||
export interface Access {
|
||||
id: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { User } from '@/core/auth';
|
||||
import { User } from '@/features/auth';
|
||||
import {
|
||||
Access,
|
||||
Doc,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { User } from '@/core/auth';
|
||||
import { User } from '@/features/auth';
|
||||
import { Doc, Role } from '@/features/docs/doc-management';
|
||||
import { Invitation, OptionType } from '@/features/docs/doc-share/types';
|
||||
import { ContentLanguage } from '@/i18n/types';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
|
||||
import { User } from '@/core/auth';
|
||||
import { User } from '@/features/auth';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
|
||||
export type UsersParams = {
|
||||
|
||||
@@ -9,8 +9,8 @@ import { css } from 'styled-components';
|
||||
|
||||
import { APIError } from '@/api';
|
||||
import { Box } from '@/components';
|
||||
import { User } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { User } from '@/features/auth';
|
||||
import { Doc, Role } from '@/features/docs';
|
||||
import { useLanguage } from '@/i18n/hooks/useLanguage';
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Button } from '@openfun/cunningham-react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { User } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { User } from '@/features/auth';
|
||||
|
||||
type Props = {
|
||||
user: User;
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
DropdownMenuOption,
|
||||
IconOptions,
|
||||
} from '@/components';
|
||||
import { User } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { User } from '@/features/auth';
|
||||
import { Doc, Role } from '@/features/docs/doc-management';
|
||||
|
||||
import { useDeleteDocInvitation, useUpdateDocInvitation } from '../api';
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
QuickSearchData,
|
||||
QuickSearchGroup,
|
||||
} from '@/components/quick-search/';
|
||||
import { User } from '@/core';
|
||||
import { User } from '@/features/auth';
|
||||
import { Access, Doc } from '@/features/docs';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
import { isValidEmail } from '@/utils';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { User } from '@/core';
|
||||
import { User } from '@/features/auth';
|
||||
|
||||
import { SearchUserRow } from './SearchUserRow';
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
QuickSearchItemContent,
|
||||
QuickSearchItemContentProps,
|
||||
} from '@/components/quick-search';
|
||||
import { User } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { User } from '@/features/auth';
|
||||
|
||||
import { UserAvatar } from './UserAvatar';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { User } from '@/core';
|
||||
import { tokens } from '@/cunningham';
|
||||
import { User } from '@/features/auth';
|
||||
|
||||
const colors = tokens.themes.default.theme.colors;
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useAuthStore } from '@/core/auth';
|
||||
import { useAuth } from '@/features/auth';
|
||||
import { Access, Role } from '@/features/docs/doc-management';
|
||||
|
||||
export const useWhoAmI = (access: Access) => {
|
||||
const { userData } = useAuthStore();
|
||||
const { user } = useAuth();
|
||||
|
||||
const isMyself = userData?.id === access.user.id;
|
||||
const isMyself = user?.id === access.user.id;
|
||||
const rolesAllowed = access.abilities.set_role_to;
|
||||
|
||||
const isLastOwner =
|
||||
!rolesAllowed.length && access.role === Role.OWNER && isMyself;
|
||||
|
||||
const isOtherOwner = access.role === Role.OWNER && userData?.id && !isMyself;
|
||||
const isOtherOwner = access.role === Role.OWNER && user?.id && !isMyself;
|
||||
|
||||
return {
|
||||
isLastOwner,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { User } from '@/core/auth';
|
||||
import { User } from '@/features/auth';
|
||||
import { Role } from '@/features/docs';
|
||||
|
||||
export interface Invitation {
|
||||
|
||||
@@ -2,10 +2,9 @@ import { useState } from 'react';
|
||||
|
||||
import { BoxButton, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { DocsBlockNoteEditor } from '@/features/docs/doc-editor';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../../doc-editor/components/BlockNoteEditor';
|
||||
|
||||
const leftPaddingMap: { [key: number]: string } = {
|
||||
3: '1.5rem',
|
||||
2: '0.9rem',
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.6305 28.8312C22.7983 28.5038 23.9166 27.9062 24.6505 26.8503C25.3749 25.8163 25.5789 24.5047 25.5789 23.2425V4.75099C25.5789 4.42358 25.5611 4.09557 25.5216 3.77148C26.1016 3.99961 26.5486 4.37658 26.8626 4.90239C27.2331 5.50024 27.4184 6.28757 27.4184 7.26435V26.0464C27.4184 27.3684 27.0942 28.3578 26.4458 29.0146C25.7974 29.6714 24.8207 29.9998 23.5155 29.9998H16.4209C16.5889 29.9704 16.7574 29.9401 16.9262 29.909C18.4067 29.6444 19.9713 29.2854 21.6185 28.8346L21.6305 28.8312Z" fill="#C9191E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.58203 25.655V6.8477C4.58203 5.70251 4.88938 4.83519 5.50408 4.24575C6.1272 3.65631 6.95242 3.33212 7.97972 3.27318C9.49542 3.18055 10.9311 3.05425 12.2868 2.89425C13.6425 2.72584 14.9393 2.53217 16.1771 2.31324C17.4234 2.0943 18.6359 1.85011 19.8148 1.58065C21.0274 1.29435 21.9578 1.4375 22.6062 2.0101C23.2546 2.58269 23.5788 3.49632 23.5788 4.75099V23.2425C23.5788 24.3456 23.3893 25.1666 23.0104 25.7055C22.6315 26.2529 21.9915 26.6528 21.0905 26.9054C19.4906 27.3433 17.9833 27.6886 16.5687 27.9412C15.154 28.2022 13.7731 28.4001 12.4258 28.5348C11.0785 28.6696 9.69751 28.7748 8.28286 28.8506C7.11241 28.918 6.20299 28.6738 5.5546 28.118C4.90622 27.5707 4.58203 26.7497 4.58203 25.655ZM9.20865 10.2624C11.0635 10.1444 12.7632 9.96305 14.3075 9.71831C14.6822 9.65722 15.0564 9.5936 15.4291 9.52759C15.8192 9.45851 16.1013 9.11859 16.1013 8.72337C16.1013 8.21154 15.638 7.82609 15.135 7.91189C14.846 7.96118 14.5555 8.00909 14.2635 8.05562C12.7346 8.29923 11.0452 8.47998 9.19523 8.5977C8.91819 8.61558 8.69776 8.70188 8.55608 8.87391C8.42209 9.03661 8.35645 9.23229 8.35645 9.45535C8.35645 9.68212 8.43296 9.87951 8.58568 10.0418L8.58783 10.0439C8.75336 10.2095 8.96369 10.2811 9.20865 10.2624ZM9.20801 14.456C11.0631 14.338 12.763 14.1566 14.3075 13.9119C15.8588 13.6589 17.3936 13.3638 18.9112 13.0266C19.2191 12.9581 19.4498 12.8503 19.5652 12.683C19.6786 12.5221 19.7347 12.3376 19.7347 12.1332C19.7347 11.9026 19.6469 11.704 19.476 11.5426C19.2921 11.3689 19.0348 11.3284 18.7304 11.3911L18.7285 11.3915C17.2823 11.7194 15.794 12.0053 14.2635 12.2492C12.7346 12.4928 11.0452 12.6735 9.19523 12.7913C8.91819 12.8091 8.69776 12.8954 8.55608 13.0675C8.42276 13.2294 8.35645 13.4205 8.35645 13.6363C8.35645 13.8703 8.43209 14.0723 8.58558 14.2354L8.59 14.2396C8.75499 14.3949 8.96316 14.4655 9.20551 14.4562L9.20801 14.456ZM9.20847 18.6494C11.0634 18.5229 12.7631 18.3374 14.3075 18.0927C15.8589 17.8482 17.3934 17.5573 18.9112 17.22C19.2199 17.1514 19.4508 17.0391 19.566 16.8627C19.6783 16.7029 19.7347 16.5233 19.7347 16.3266C19.7347 16.0961 19.6469 15.8974 19.476 15.7361C19.2921 15.5623 19.0348 15.5218 18.7304 15.5845L18.729 15.5848C17.2827 15.9043 15.7942 16.1861 14.2635 16.43C12.7345 16.6736 11.045 16.8586 9.19495 16.9847C8.91804 17.0026 8.69771 17.0889 8.55608 17.2609C8.42276 17.4228 8.35645 17.6139 8.35645 17.8297C8.35645 18.0637 8.43209 18.2658 8.58558 18.4289L8.59 18.433C8.75499 18.5883 8.96316 18.6589 9.20551 18.6496L9.20847 18.6494ZM14.3075 22.257C12.7632 22.5018 11.0635 22.6831 9.20867 22.8012C8.9637 22.8198 8.75337 22.7482 8.58783 22.5826L8.58572 22.5805C8.433 22.4182 8.35645 22.2208 8.35645 21.9941C8.35645 21.771 8.42209 21.5753 8.55608 21.4126C8.69776 21.2406 8.91827 21.1543 9.19531 21.1364C11.0453 21.0187 12.7346 20.838 14.2635 20.5943C14.5555 20.5478 14.846 20.4999 15.135 20.4506C15.638 20.3648 16.1013 20.7503 16.1013 21.2621C16.1013 21.6573 15.8192 21.9972 15.4291 22.0663C15.0564 22.1323 14.6822 22.1959 14.3075 22.257Z" fill="#000091"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,26 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Icon } from '@/components/';
|
||||
import { useLeftPanelStore } from '@/features/left-panel';
|
||||
|
||||
export const ButtonTogglePanel = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isPanelOpen, togglePanel } = useLeftPanelStore();
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="medium"
|
||||
onClick={() => togglePanel()}
|
||||
aria-label={t('Open the header menu')}
|
||||
color="tertiary-text"
|
||||
icon={
|
||||
<Icon
|
||||
$variation="800"
|
||||
$theme="primary"
|
||||
iconName={isPanelOpen ? 'close' : 'menu'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,25 +1,23 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, StyledLink } from '@/components/';
|
||||
import { ButtonLogin } from '@/core/auth';
|
||||
import { default as IconDocs } from '@/assets/icons/icon-docs.svg?url';
|
||||
import { Box, StyledLink } from '@/components/';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { ButtonLogin } from '@/features/auth';
|
||||
import { LanguagePicker } from '@/features/language';
|
||||
import { useLeftPanelStore } from '@/features/left-panel';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { default as IconDocs } from '../assets/icon-docs.svg?url';
|
||||
import { HEADER_HEIGHT } from '../conf';
|
||||
|
||||
import { ButtonTogglePanel } from './ButtonTogglePanel';
|
||||
import { LaGaufre } from './LaGaufre';
|
||||
import Title from './Title/Title';
|
||||
import { Title } from './Title';
|
||||
|
||||
export const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useCunninghamTheme();
|
||||
const { isPanelOpen, togglePanel } = useLeftPanelStore();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
|
||||
const spacings = theme.spacingsTokens();
|
||||
@@ -29,7 +27,6 @@ export const Header = () => {
|
||||
<Box
|
||||
as="header"
|
||||
$css={css`
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -39,27 +36,12 @@ export const Header = () => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: ${HEADER_HEIGHT}px;
|
||||
min-height: ${HEADER_HEIGHT}px;
|
||||
padding: 0 ${spacings['base']};
|
||||
background-color: ${colors['greyscale-000']};
|
||||
border-bottom: 1px solid ${colors['greyscale-200']};
|
||||
`}
|
||||
>
|
||||
{!isDesktop && (
|
||||
<Button
|
||||
size="medium"
|
||||
onClick={() => togglePanel()}
|
||||
aria-label={t('Open the header menu')}
|
||||
color="tertiary-text"
|
||||
icon={
|
||||
<Icon
|
||||
$variation="800"
|
||||
$theme="primary"
|
||||
iconName={isPanelOpen ? 'close' : 'menu'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!isDesktop && <ButtonTogglePanel />}
|
||||
<StyledLink href="/">
|
||||
<Box
|
||||
$align="center"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Text } from '@/components/';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
const Title = () => {
|
||||
export const Title = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useCunninghamTheme();
|
||||
const spacings = theme.spacingsTokens();
|
||||
@@ -11,16 +12,30 @@ const Title = () => {
|
||||
|
||||
return (
|
||||
<Box $direction="row" $align="center" $gap={spacings['2xs']}>
|
||||
<Text $margin="none" as="h2" $color="#000091" $zIndex={1} $size="1.30rem">
|
||||
<Text
|
||||
$margin="none"
|
||||
as="h2"
|
||||
$color="#000091"
|
||||
$zIndex={1}
|
||||
$size="1.375rem"
|
||||
>
|
||||
{t('Docs')}
|
||||
</Text>
|
||||
<Text
|
||||
$padding={{ horizontal: 'xs', vertical: '1px' }}
|
||||
$padding={{
|
||||
horizontal: '6px',
|
||||
vertical: '4px',
|
||||
}}
|
||||
$size="11px"
|
||||
$theme="primary"
|
||||
$variation="500"
|
||||
$weight="bold"
|
||||
$radius="12px"
|
||||
$css={css`
|
||||
line-height: 9px;
|
||||
`}
|
||||
$width="40px"
|
||||
$height="16px"
|
||||
$background={colors['primary-200']}
|
||||
>
|
||||
BETA
|
||||
@@ -28,5 +43,3 @@ const Title = () => {
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Title;
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './ButtonTogglePanel';
|
||||
export * from './Header';
|
||||
export * from './LaGaufre';
|
||||
export * from './Title';
|
||||
@@ -1 +1,2 @@
|
||||
export * from './components/Header';
|
||||
export * from './components/';
|
||||
export * from './conf';
|
||||
|
||||