Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59ca5c12a3 | |||
| c94f26c8b9 | |||
| fc2f14b3f4 | |||
| 6dd1697915 | |||
| 79e899c301 | |||
| 2194301716 | |||
| 0348894ab8 | |||
| 9b17d8bea1 | |||
| 69d6b6f934 | |||
| 6c106374fa | |||
| af039d045d | |||
| 4c9caf09ba | |||
| 3fd02adbec | |||
| 90dac3cd15 | |||
| d0307ee6d9 | |||
| 09d02b7ced | |||
| 56a26d9663 | |||
| 42f809f6d4 | |||
| 7d64c82987 | |||
| 6252227bb6 | |||
| e9ac393a8f | |||
| 5b1745f991 | |||
| 0e55bf5c43 | |||
| 9f66f73501 | |||
| c3da28b07f | |||
| b035b96dec | |||
| 9623ac4141 | |||
| c8edbd285b | |||
| 016597d5a2 | |||
| 52dea8fa2f | |||
| 0a37a8ea6d | |||
| c1404ef904 | |||
| 2c0fce61df | |||
| bbe9b6b6cf | |||
| 23231563c9 | |||
| d75c8668c5 | |||
| f266232b5a | |||
| a8362e8e88 | |||
| e4dfae1905 | |||
| a09e740648 | |||
| 5ee6a43f08 | |||
| 8bd83cbfcd | |||
| bc14d1d0f8 |
@@ -8,6 +8,9 @@ on:
|
||||
- 'main'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
|
||||
env:
|
||||
DOCKER_USER: 1001:127
|
||||
@@ -52,6 +55,7 @@ jobs:
|
||||
with:
|
||||
docker-build-args: '--target backend-production -f Dockerfile'
|
||||
docker-image-name: 'docker.io/lasuite/impress-backend:${{ github.sha }}'
|
||||
continue-on-error: true
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -102,6 +106,7 @@ jobs:
|
||||
with:
|
||||
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
|
||||
docker-image-name: 'docker.io/lasuite/impress-frontend:${{ github.sha }}'
|
||||
continue-on-error: true
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -153,6 +158,7 @@ jobs:
|
||||
with:
|
||||
docker-build-args: '-f src/frontend/Dockerfile --target y-provider'
|
||||
docker-image-name: 'docker.io/lasuite/impress-frontend:${{ github.sha }}'
|
||||
continue-on-error: true
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Helmfile lint
|
||||
run-name: Helmfile lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
helmfile-lint:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/helmfile/helmfile:latest
|
||||
steps:
|
||||
-
|
||||
uses: numerique-gouv/action-helmfile-lint@main
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
helmfile-src: "src/helm"
|
||||
repositories: "impress,secrets"
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
- name: Run e2e tests
|
||||
run: cd src/frontend/ && yarn e2e:test --project='chromium'
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-chromium-report
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
- name: Run e2e tests
|
||||
run: cd src/frontend/ && yarn e2e:test --project=firefox --project=webkit
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-other-report
|
||||
|
||||
@@ -107,7 +107,9 @@ jobs:
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
python-version: "3.12.6"
|
||||
- name: Upgrade pip and setuptools
|
||||
run: pip install --upgrade pip setuptools
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
- name: Check code formatting with ruff
|
||||
@@ -199,7 +201,7 @@ jobs:
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
python-version: "3.12.6"
|
||||
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
|
||||
+27
-2
@@ -9,18 +9,41 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [1.8.1] - 2024-11-27
|
||||
|
||||
## Fixed
|
||||
|
||||
🐛(frontend) link not clickable and flickering firefox #457
|
||||
|
||||
|
||||
## [1.8.0] - 2024-11-25
|
||||
|
||||
## Added
|
||||
|
||||
- 🌐(backend) add german translation #259
|
||||
- 🌐(frontend) Add German translation #255
|
||||
- ✨(frontend) Add a broadcast store #387
|
||||
- ✨(backend) whitelist pod's IP address #443
|
||||
- ✨(backend) config endpoint #425
|
||||
- ✨(frontend) config endpoint #424
|
||||
- ✨(frontend) add sentry #424
|
||||
- ✨(frontend) add crisp chatbot #450
|
||||
|
||||
## Changed
|
||||
|
||||
- 🚸(backend) improve users similarity search and sort results #391
|
||||
- 🌐(backend) add german translation #259
|
||||
- ♻️(frontend) simplify stores #402
|
||||
- ✨(frontend) update $css Box props type to add styled components RuleSet #423
|
||||
- ✅(CI) trivy continue on error #453
|
||||
|
||||
## Fixed
|
||||
|
||||
- 🔧(backend) fix logging for docker and make it configurable by envar #427
|
||||
- 🦺(backend) add comma to sub regex #408
|
||||
- 🐛(editor) collaborative user tag hidden when read only #385
|
||||
- 🐛(frontend) users have view access when revoked #387
|
||||
- 🐛(frontend) fix placeholder editable when double clicks #454
|
||||
|
||||
|
||||
## [1.7.0] - 2024-10-24
|
||||
@@ -252,7 +275,9 @@ and this project adheres to
|
||||
- 🚀 Impress, project to manage your documents easily and collaboratively.
|
||||
|
||||
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v1.7.0...main
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v1.8.1...main
|
||||
[v1.8.1]: https://github.com/numerique-gouv/impress/releases/v1.8.1
|
||||
[v1.8.0]: https://github.com/numerique-gouv/impress/releases/v1.8.0
|
||||
[v1.7.0]: https://github.com/numerique-gouv/impress/releases/v1.7.0
|
||||
[v1.6.0]: https://github.com/numerique-gouv/impress/releases/v1.6.0
|
||||
[1.5.1]: https://github.com/numerique-gouv/impress/releases/v1.5.1
|
||||
|
||||
@@ -4,6 +4,12 @@ DJANGO_SECRET_KEY=ThisIsAnExampleKeyForDevPurposeOnly
|
||||
DJANGO_SETTINGS_MODULE=impress.settings
|
||||
DJANGO_SUPERUSER_PASSWORD=admin
|
||||
|
||||
# Logging
|
||||
# Set to DEBUG level for dev only
|
||||
LOGGING_LEVEL_HANDLERS_CONSOLE=INFO
|
||||
LOGGING_LEVEL_LOGGERS_ROOT=INFO
|
||||
LOGGING_LEVEL_LOGGERS_APP=INFO
|
||||
|
||||
# Python
|
||||
PYTHONPATH=/app
|
||||
|
||||
@@ -21,6 +27,7 @@ STORAGES_STATICFILES_BACKEND=django.contrib.staticfiles.storage.StaticFilesStora
|
||||
AWS_S3_ENDPOINT_URL=http://minio:9000
|
||||
AWS_S3_ACCESS_KEY_ID=impress
|
||||
AWS_S3_SECRET_ACCESS_KEY=password
|
||||
MEDIA_BASE_URL=http://localhost:8083
|
||||
|
||||
# OIDC
|
||||
OIDC_OP_JWKS_ENDPOINT=http://nginx:8083/realms/impress/protocol/openid-connect/certs
|
||||
@@ -44,3 +51,9 @@ OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
|
||||
AI_BASE_URL=https://openaiendpoint.com
|
||||
AI_API_KEY=password
|
||||
AI_MODEL=llama
|
||||
|
||||
# Collaboration
|
||||
COLLABORATION_SERVER_URL=ws://localhost:4444
|
||||
|
||||
# Frontend
|
||||
FRONTEND_THEME=dsfr
|
||||
|
||||
@@ -26,11 +26,13 @@ from rest_framework import (
|
||||
mixins,
|
||||
pagination,
|
||||
status,
|
||||
views,
|
||||
viewsets,
|
||||
)
|
||||
from rest_framework import (
|
||||
response as drf_response,
|
||||
)
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
from core import enums, models
|
||||
from core.services.ai_services import AIService
|
||||
@@ -886,3 +888,31 @@ class InvitationViewset(
|
||||
invitation.document.email_invitation(
|
||||
language, invitation.email, invitation.role, self.request.user
|
||||
)
|
||||
|
||||
|
||||
class ConfigView(views.APIView):
|
||||
"""API ViewSet for sharing some public settings."""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def get(self, request):
|
||||
"""
|
||||
GET /api/v1.0/config/
|
||||
Return a dictionary of public settings.
|
||||
"""
|
||||
array_settings = [
|
||||
"COLLABORATION_SERVER_URL",
|
||||
"CRISP_WEBSITE_ID",
|
||||
"ENVIRONMENT",
|
||||
"FRONTEND_THEME",
|
||||
"MEDIA_BASE_URL",
|
||||
"LANGUAGES",
|
||||
"LANGUAGE_CODE",
|
||||
"SENTRY_DSN",
|
||||
]
|
||||
dict_settings = {}
|
||||
for setting in array_settings:
|
||||
if hasattr(settings, setting):
|
||||
dict_settings[setting] = getattr(settings, setting)
|
||||
|
||||
return drf_response.Response(dict_settings)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Test config API endpoints in the Impress core app.
|
||||
"""
|
||||
|
||||
from django.test import override_settings
|
||||
|
||||
import pytest
|
||||
from rest_framework.status import (
|
||||
HTTP_200_OK,
|
||||
)
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@override_settings(
|
||||
COLLABORATION_SERVER_URL="http://testcollab/",
|
||||
CRISP_WEBSITE_ID="123",
|
||||
FRONTEND_THEME="test-theme",
|
||||
MEDIA_BASE_URL="http://testserver/",
|
||||
SENTRY_DSN="https://sentry.test/123",
|
||||
)
|
||||
@pytest.mark.parametrize("is_authenticated", [False, True])
|
||||
def test_api_config(is_authenticated):
|
||||
"""Anonymous users should be allowed to get the configuration."""
|
||||
client = APIClient()
|
||||
|
||||
if is_authenticated:
|
||||
user = factories.UserFactory()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get("/api/v1.0/config/")
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"COLLABORATION_SERVER_URL": "http://testcollab/",
|
||||
"CRISP_WEBSITE_ID": "123",
|
||||
"ENVIRONMENT": "test",
|
||||
"FRONTEND_THEME": "test-theme",
|
||||
"LANGUAGES": [["en-us", "English"], ["fr-fr", "French"], ["de-de", "German"]],
|
||||
"LANGUAGE_CODE": "en-us",
|
||||
"MEDIA_BASE_URL": "http://testserver/",
|
||||
"SENTRY_DSN": "https://sentry.test/123",
|
||||
}
|
||||
@@ -55,4 +55,5 @@ urlpatterns = [
|
||||
]
|
||||
),
|
||||
),
|
||||
path(f"api/{settings.API_VERSION}/config/", viewsets.ConfigView.as_view()),
|
||||
]
|
||||
|
||||
@@ -10,8 +10,9 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import tomllib
|
||||
from socket import gethostbyname, gethostname
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -27,19 +28,12 @@ DATA_DIR = os.path.join("/", "data")
|
||||
def get_release():
|
||||
"""
|
||||
Get the current release of the application
|
||||
|
||||
By release, we mean the release from the version.json file à la Mozilla [1]
|
||||
(if any). If this file has not been found, it defaults to "NA".
|
||||
|
||||
[1]
|
||||
https://github.com/mozilla-services/Dockerflow/blob/master/docs/version_object.md
|
||||
"""
|
||||
# Try to get the current release from the version.json file generated by the
|
||||
# CI during the Docker image build
|
||||
try:
|
||||
with open(os.path.join(BASE_DIR, "version.json"), encoding="utf8") as version:
|
||||
return json.load(version)["version"]
|
||||
except FileNotFoundError:
|
||||
with open(os.path.join(BASE_DIR, "pyproject.toml"), "rb") as f:
|
||||
pyproject_data = tomllib.load(f)
|
||||
return pyproject_data["project"]["version"]
|
||||
except (FileNotFoundError, KeyError):
|
||||
return "NA" # Default: not available
|
||||
|
||||
|
||||
@@ -56,7 +50,7 @@ class Base(Configuration):
|
||||
You may also want to override default configuration by setting the following environment
|
||||
variables:
|
||||
|
||||
* DJANGO_SENTRY_DSN
|
||||
* SENTRY_DSN
|
||||
* DB_NAME
|
||||
* DB_HOST
|
||||
* DB_PASSWORD
|
||||
@@ -104,6 +98,9 @@ class Base(Configuration):
|
||||
STATIC_ROOT = os.path.join(DATA_DIR, "static")
|
||||
MEDIA_URL = "/media/"
|
||||
MEDIA_ROOT = os.path.join(DATA_DIR, "media")
|
||||
MEDIA_BASE_URL = values.Value(
|
||||
None, environ_name="MEDIA_BASE_URL", environ_prefix=None
|
||||
)
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
@@ -372,7 +369,22 @@ class Base(Configuration):
|
||||
CORS_ALLOWED_ORIGIN_REGEXES = values.ListValue([])
|
||||
|
||||
# Sentry
|
||||
SENTRY_DSN = values.Value(None, environ_name="SENTRY_DSN")
|
||||
SENTRY_DSN = values.Value(None, environ_name="SENTRY_DSN", environ_prefix=None)
|
||||
|
||||
# Collaboration
|
||||
COLLABORATION_SERVER_URL = values.Value(
|
||||
None, environ_name="COLLABORATION_SERVER_URL", environ_prefix=None
|
||||
)
|
||||
|
||||
# Frontend
|
||||
FRONTEND_THEME = values.Value(
|
||||
None, environ_name="FRONTEND_THEME", environ_prefix=None
|
||||
)
|
||||
|
||||
# Crisp
|
||||
CRISP_WEBSITE_ID = values.Value(
|
||||
None, environ_name="CRISP_WEBSITE_ID", environ_prefix=None
|
||||
)
|
||||
|
||||
# Easy thumbnails
|
||||
THUMBNAIL_EXTENSION = "webp"
|
||||
@@ -482,6 +494,42 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Logging
|
||||
# We want to make it easy to log to console but by default we log production
|
||||
# to Sentry and don't want to log to console.
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": values.Value(
|
||||
"ERROR",
|
||||
environ_name="LOGGING_LEVEL_HANDLERS_CONSOLE",
|
||||
environ_prefix=None,
|
||||
),
|
||||
},
|
||||
},
|
||||
# Override root logger to send it to console
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": values.Value(
|
||||
"INFO", environ_name="LOGGING_LEVEL_LOGGERS_ROOT", environ_prefix=None
|
||||
),
|
||||
},
|
||||
"loggers": {
|
||||
"core": {
|
||||
"handlers": ["console"],
|
||||
"level": values.Value(
|
||||
"INFO",
|
||||
environ_name="LOGGING_LEVEL_LOGGERS_APP",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@property
|
||||
def ENVIRONMENT(self):
|
||||
@@ -577,23 +625,6 @@ class Development(Base):
|
||||
class Test(Base):
|
||||
"""Test environment settings"""
|
||||
|
||||
LOGGING = values.DictValue(
|
||||
{
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"impress": {
|
||||
"handlers": ["console"],
|
||||
"level": "DEBUG",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
PASSWORD_HASHERS = [
|
||||
"django.contrib.auth.hashers.MD5PasswordHasher",
|
||||
]
|
||||
@@ -624,7 +655,13 @@ class Production(Base):
|
||||
"""
|
||||
|
||||
# Security
|
||||
ALLOWED_HOSTS = values.ListValue(None)
|
||||
# Add allowed host from environment variables.
|
||||
# The machine hostname is added by default,
|
||||
# it makes the application pingable by a load balancer on the same machine by example
|
||||
ALLOWED_HOSTS = [
|
||||
*values.ListValue([], environ_name="ALLOWED_HOSTS"),
|
||||
gethostbyname(gethostname()),
|
||||
]
|
||||
CSRF_TRUSTED_ORIGINS = values.ListValue([])
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "1.7.0"
|
||||
version = "1.8.1"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -17,13 +17,13 @@ classifiers = [
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Natural Language :: English",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
description = "An application to print markdown to pdf from a set of managed templates."
|
||||
keywords = ["Django", "Contacts", "Templates", "RBAC"]
|
||||
license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"boto3==1.35.44",
|
||||
"Brotli==1.1.0",
|
||||
@@ -127,6 +127,7 @@ select = [
|
||||
[tool.ruff.lint.isort]
|
||||
section-order = ["future","standard-library","django","third-party","impress","first-party","local-folder"]
|
||||
sections = { impress=["core"], django=["django"] }
|
||||
extra-standard-library = ["tomllib"]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"**/tests/*" = ["S", "SLF"]
|
||||
|
||||
@@ -61,18 +61,9 @@ FROM impress AS impress-builder
|
||||
|
||||
WORKDIR /home/frontend/apps/impress
|
||||
|
||||
ARG FRONTEND_THEME
|
||||
ENV NEXT_PUBLIC_THEME=${FRONTEND_THEME}
|
||||
|
||||
ARG Y_PROVIDER_URL
|
||||
ENV NEXT_PUBLIC_Y_PROVIDER_URL=${Y_PROVIDER_URL}
|
||||
|
||||
ARG API_ORIGIN
|
||||
ENV NEXT_PUBLIC_API_ORIGIN=${API_ORIGIN}
|
||||
|
||||
ARG MEDIA_URL
|
||||
ENV NEXT_PUBLIC_MEDIA_URL=${MEDIA_URL}
|
||||
|
||||
ARG SW_DEACTIVATED
|
||||
ENV NEXT_PUBLIC_SW_DEACTIVATED=${SW_DEACTIVATED}
|
||||
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import path from 'path';
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc } from './common';
|
||||
|
||||
const config = {
|
||||
CRISP_WEBSITE_ID: null,
|
||||
COLLABORATION_SERVER_URL: 'ws://localhost:4444',
|
||||
ENVIRONMENT: 'development',
|
||||
FRONTEND_THEME: 'dsfr',
|
||||
MEDIA_BASE_URL: 'http://localhost:8083',
|
||||
LANGUAGES: [
|
||||
['en-us', 'English'],
|
||||
['fr-fr', 'French'],
|
||||
['de-de', 'German'],
|
||||
],
|
||||
LANGUAGE_CODE: 'en-us',
|
||||
SENTRY_DSN: null,
|
||||
};
|
||||
|
||||
test.describe('Config', () => {
|
||||
test('it checks the config api is called', 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();
|
||||
|
||||
expect(await response.json()).toStrictEqual(config);
|
||||
});
|
||||
|
||||
test('it checks that sentry is trying to init from config endpoint', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route('**/api/v1.0/config/', async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method().includes('GET')) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
...config,
|
||||
SENTRY_DSN: 'https://sentry.io/123',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
const invalidMsg = 'Invalid Sentry Dsn: https://sentry.io/123';
|
||||
const consoleMessage = page.waitForEvent('console', {
|
||||
timeout: 5000,
|
||||
predicate: (msg) => msg.text().includes(invalidMsg),
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
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,
|
||||
}) => {
|
||||
await page.goto('/');
|
||||
|
||||
await createDoc(page, 'doc-media', browserName, 1);
|
||||
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Resizable image with caption').click();
|
||||
await page.getByText('Upload image').click();
|
||||
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(
|
||||
path.join(__dirname, 'assets/logo-suite-numerique.png'),
|
||||
);
|
||||
|
||||
const image = page.getByRole('img', { name: 'logo-suite-numerique.png' });
|
||||
|
||||
await expect(image).toBeVisible();
|
||||
|
||||
// Check src of image
|
||||
expect(await image.getAttribute('src')).toMatch(
|
||||
/http:\/\/localhost:8083\/media\/.*\/attachments\/.*.png/,
|
||||
);
|
||||
});
|
||||
|
||||
test('it checks that collaboration server is configured from config endpoint', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const webSocketPromise = page.waitForEvent('websocket', (webSocket) => {
|
||||
return webSocket.url().includes('ws://localhost:4444/');
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const randomDoc = await createDoc(
|
||||
page,
|
||||
'doc-collaboration',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
await expect(page.locator('h2').getByText(randomDoc[0])).toBeVisible();
|
||||
|
||||
const webSocket = await webSocketPromise;
|
||||
expect(webSocket.url()).toContain('ws://localhost:4444/');
|
||||
});
|
||||
|
||||
test('it checks that Crisp is trying to init from config endpoint', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route('**/api/v1.0/config/', async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method().includes('GET')) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
...config,
|
||||
CRISP_WEBSITE_ID: '1234',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
await expect(
|
||||
page.locator('#crisp-chatbox').getByText('Invalid website'),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
@@ -12,7 +12,7 @@
|
||||
"test:ui::chromium": "yarn test:ui --project=chromium"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.48.1",
|
||||
"@playwright/test": "1.49.0",
|
||||
"@types/node": "*",
|
||||
"@types/pdf-parse": "1.1.4",
|
||||
"eslint-config-impress": "*",
|
||||
|
||||
@@ -1,5 +1,2 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=
|
||||
NEXT_PUBLIC_Y_PROVIDER_URL=
|
||||
NEXT_PUBLIC_MEDIA_URL=
|
||||
NEXT_PUBLIC_SW_DEACTIVATED=
|
||||
NEXT_PUBLIC_THEME=open-desk
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
|
||||
NEXT_PUBLIC_Y_PROVIDER_URL=ws://localhost:4444
|
||||
NEXT_PUBLIC_MEDIA_URL=http://localhost:8083
|
||||
NEXT_PUBLIC_SW_DEACTIVATED=true
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=http://test.jest
|
||||
NEXT_PUBLIC_THEME=test-theme
|
||||
|
||||
@@ -399,206 +399,6 @@ const config = {
|
||||
},
|
||||
},
|
||||
},
|
||||
'open-desk': {
|
||||
theme: {
|
||||
colors: {
|
||||
'card-border': '#ededed',
|
||||
'primary-text': '#637089',
|
||||
'primary-100': '#F7F5FF',
|
||||
'primary-200': '#ECE7FE',
|
||||
'primary-300': '#DCD2FE',
|
||||
'primary-400': '#C8B9FD',
|
||||
'primary-500': '#8E75FA',
|
||||
'primary-600': '#7051FA',
|
||||
'primary-700': '#571EFA',
|
||||
'primary-800': '#4519C2',
|
||||
'primary-900': '#341291',
|
||||
'secondary-text': '#FFFFFF',
|
||||
'secondary-100': '#EDFDFB',
|
||||
'secondary-200': '#BFF9F2',
|
||||
'secondary-300': '#71EFE1',
|
||||
'secondary-400': '#00E6CC',
|
||||
'secondary-500': '#00A896',
|
||||
'secondary-600': '#008A7B',
|
||||
'secondary-700': '#006C60',
|
||||
'secondary-800': '#00564D',
|
||||
'secondary-900': '#004039',
|
||||
'greyscale-text': '#303C4B',
|
||||
'greyscale-000': '#ffffff',
|
||||
'greyscale-100': '#EEEFF2',
|
||||
'greyscale-200': '#D3D7DE',
|
||||
'greyscale-300': '#B6BCC8',
|
||||
'greyscale-400': '#7C879C',
|
||||
'greyscale-500': '#637089',
|
||||
'greyscale-600': '#4D5B79',
|
||||
'greyscale-700': '#364768',
|
||||
'greyscale-800': '#203257',
|
||||
'greyscale-900': '#1e1e1e',
|
||||
'success-text': '#1f8d49',
|
||||
'success-100': '#dffee6',
|
||||
'success-200': '#b8fec9',
|
||||
'success-300': '#88fdaa',
|
||||
'success-400': '#3bea7e',
|
||||
'success-500': '#1f8d49',
|
||||
'success-600': '#18753c',
|
||||
'success-700': '#204129',
|
||||
'success-800': '#1e2e22',
|
||||
'success-900': '#19281d',
|
||||
'info-text': '#0078f3',
|
||||
'info-100': '#f4f6ff',
|
||||
'info-200': '#e8edff',
|
||||
'info-300': '#dde5ff',
|
||||
'info-400': '#bdcdff',
|
||||
'info-500': '#0078f3',
|
||||
'info-600': '#0063cb',
|
||||
'info-700': '#f4f6ff',
|
||||
'info-800': '#222a3f',
|
||||
'info-900': '#1d2437',
|
||||
'warning-text': '#d64d00',
|
||||
'warning-100': '#fff4f3',
|
||||
'warning-200': '#ffe9e6',
|
||||
'warning-300': '#ffded9',
|
||||
'warning-400': '#ffbeb4',
|
||||
'warning-500': '#d64d00',
|
||||
'warning-600': '#b34000',
|
||||
'warning-700': '#5e2c21',
|
||||
'warning-800': '#3e241e',
|
||||
'warning-900': '#361e19',
|
||||
'danger-text': '#e1000f',
|
||||
'danger-100': '#fef4f4',
|
||||
'danger-200': '#fee9e9',
|
||||
'danger-300': '#fddede',
|
||||
'danger-400': '#fcbfbf',
|
||||
'danger-500': '#e1000f',
|
||||
'danger-600': '#c9191e',
|
||||
'danger-700': '#642727',
|
||||
'danger-800': '#412121',
|
||||
'danger-900': '#3a1c1c',
|
||||
},
|
||||
font: {
|
||||
families: {
|
||||
accent: 'Open Sans',
|
||||
base: 'Open Sans',
|
||||
},
|
||||
},
|
||||
logo: {
|
||||
src: '/assets/logo-open-desk.svg',
|
||||
widthHeader: '110px',
|
||||
widthFooter: '220px',
|
||||
alt: 'OpenDesk Logo',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
alert: {
|
||||
'border-radius': '0',
|
||||
},
|
||||
button: {
|
||||
'medium-height': '48px',
|
||||
'border-radius': '4px',
|
||||
primary: {
|
||||
background: {
|
||||
color: 'var(--c--theme--colors--primary-700)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-900)',
|
||||
'color-active': 'var(--c--theme--colors--primary-900)',
|
||||
},
|
||||
color: '#ffffff',
|
||||
'color-hover': '#ffffff',
|
||||
'color-active': '#ffffff',
|
||||
},
|
||||
'primary-text': {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
'color-active': 'var(--c--theme--colors--primary-100)',
|
||||
},
|
||||
'color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
},
|
||||
secondary: {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
'color-active': '#EDEDED',
|
||||
},
|
||||
border: {
|
||||
color: 'var(--c--theme--colors--primary-600)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-600)',
|
||||
},
|
||||
color: 'var(--c--theme--colors--primary-600)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-700)',
|
||||
},
|
||||
'tertiary-text': {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
},
|
||||
color: 'var(--c--theme--colors--primary-600)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-700)',
|
||||
},
|
||||
},
|
||||
datagrid: {
|
||||
header: {
|
||||
color: 'var(--c--theme--colors--primary-500)',
|
||||
size: 'var(--c--theme--font--sizes--s)',
|
||||
},
|
||||
body: {
|
||||
'background-color': 'transparent',
|
||||
'background-color-hover': '#F4F4FD',
|
||||
},
|
||||
pagination: {
|
||||
'background-color': 'var(--c--theme--colors--primary-100)',
|
||||
'border-color': 'var(--c--theme--colors--primary-600)',
|
||||
'background-color-active': 'var(--c--theme--colors--primary-300)',
|
||||
},
|
||||
},
|
||||
'forms-checkbox': {
|
||||
'border-radius': '0',
|
||||
color: 'var(--c--theme--colors--greyscale-600)',
|
||||
text: {
|
||||
color: 'var(--c--theme--colors--greyscale-600)',
|
||||
size: 'var(--c--theme--font--sizes--t)',
|
||||
},
|
||||
},
|
||||
'forms-datepicker': {
|
||||
'border-radius': '0',
|
||||
},
|
||||
'forms-fileuploader': {
|
||||
'border-radius': '0',
|
||||
},
|
||||
'forms-field': {
|
||||
color: 'var(--c--theme--colors--primary-600)',
|
||||
},
|
||||
'forms-input': {
|
||||
'border-radius': '4px',
|
||||
'background-color': '#ffffff',
|
||||
'border-color': 'var(--c--theme--colors--primary-600)',
|
||||
'box-shadow-color': 'var(--c--theme--colors--primary-600)',
|
||||
'value-color': 'var(--c--theme--colors--primary-600)',
|
||||
'font-size': '14px',
|
||||
},
|
||||
'forms-labelledbox': {
|
||||
'label-color': {
|
||||
big: 'var(--c--theme--colors--primary-600)',
|
||||
},
|
||||
},
|
||||
'forms-select': {
|
||||
'item-font-size': '14px',
|
||||
'border-radius': '4px',
|
||||
'border-radius-hover': '4px',
|
||||
'background-color': '#ffffff',
|
||||
'border-color': 'var(--c--theme--colors--primary-600)',
|
||||
'border-color-hover': 'var(--c--theme--colors--primary-600)',
|
||||
'box-shadow-color': 'var(--c--theme--colors--primary-600)',
|
||||
},
|
||||
'forms-radio': {
|
||||
'accent-color': 'var(--c--theme--colors--primary-600)',
|
||||
},
|
||||
'forms-switch': {
|
||||
'handle-border-radius': '2px',
|
||||
'rail-border-radius': '4px',
|
||||
'accent-color': 'var(--c--theme--colors--primary-600)',
|
||||
},
|
||||
'forms-textarea': {
|
||||
'border-radius': '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -19,37 +19,39 @@
|
||||
"@blocknote/mantine": "*",
|
||||
"@blocknote/react": "*",
|
||||
"@gouvfr-lasuite/integration": "1.0.2",
|
||||
"@hocuspocus/provider": "2.13.7",
|
||||
"@hocuspocus/provider": "2.14.0",
|
||||
"@openfun/cunningham-react": "2.9.4",
|
||||
"@tanstack/react-query": "5.59.15",
|
||||
"i18next": "23.16.2",
|
||||
"@sentry/nextjs": "8.40.0",
|
||||
"@tanstack/react-query": "5.61.3",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"i18next": "24.0.0",
|
||||
"i18next-browser-languagedetector": "8.0.0",
|
||||
"idb": "8.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.5.0",
|
||||
"next": "14.2.15",
|
||||
"next": "15.0.3",
|
||||
"react": "*",
|
||||
"react-aria-components": "1.4.1",
|
||||
"react-aria-components": "1.5.0",
|
||||
"react-dom": "*",
|
||||
"react-i18next": "15.0.3",
|
||||
"react-select": "5.8.1",
|
||||
"react-i18next": "15.1.1",
|
||||
"react-select": "5.8.3",
|
||||
"styled-components": "6.1.13",
|
||||
"y-protocols": "1.0.6",
|
||||
"yjs": "*",
|
||||
"zustand": "5.0.0"
|
||||
"zustand": "5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@tanstack/react-query-devtools": "5.59.15",
|
||||
"@tanstack/react-query-devtools": "5.61.3",
|
||||
"@testing-library/dom": "10.4.0",
|
||||
"@testing-library/jest-dom": "6.6.2",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "16.0.1",
|
||||
"@testing-library/user-event": "14.5.2",
|
||||
"@types/jest": "29.5.13",
|
||||
"@types/lodash": "4.17.12",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lodash": "4.17.13",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/node": "*",
|
||||
"@types/react": "18.3.11",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "*",
|
||||
"cross-env": "*",
|
||||
"dotenv": "16.4.5",
|
||||
@@ -63,7 +65,7 @@
|
||||
"stylelint-config-standard": "36.0.1",
|
||||
"stylelint-prettier": "5.0.2",
|
||||
"typescript": "*",
|
||||
"webpack": "5.95.0",
|
||||
"webpack": "5.96.1",
|
||||
"workbox-webpack-plugin": "7.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<svg
|
||||
width="128"
|
||||
height="40"
|
||||
viewBox="0 0 128 40"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M23.2902 26.2256V26.6237C23.2902 28.3039 21.9405 29.6701 20.2806 29.6701H13.2164C11.5565 29.6701 10.2068 28.3039 10.2068 26.6237V19.4729C10.2068 17.7927 11.5565 16.4265 13.2164 16.4265H13.6097V26.2274H23.2921L23.2902 26.2256Z"
|
||||
fill="#927AFA"
|
||||
/>
|
||||
<path
|
||||
d="M23.4214 23.8407H15.9639V15.4957C15.9639 12.6474 18.2534 10.3299 21.0673 10.3299H23.4232C26.6692 10.3299 29.3113 13.0044 29.3113 16.2901V17.8787C29.3113 21.1644 26.6692 23.8389 23.4232 23.8389L23.4214 23.8407ZM19.3649 20.3962H23.4214C24.7914 20.3962 25.9066 19.2673 25.9066 17.8806V16.2919C25.9066 14.9051 24.7914 13.7763 23.4214 13.7763H21.0654C20.1274 13.7763 19.3649 14.5482 19.3649 15.4976V20.3981V20.3962Z"
|
||||
fill="#571EFA"
|
||||
/>
|
||||
<path
|
||||
d="M38.9254 16.9629C41.6673 16.9629 43.4195 19.0001 43.4195 21.4952C43.4195 23.9903 41.6673 26.0275 38.9254 26.0275C36.1835 26.0275 34.4128 23.9903 34.4128 21.4952C34.4128 19.0001 36.1651 16.9629 38.9254 16.9629ZM38.9254 24.921C41.0247 24.921 42.1362 23.3399 42.1362 21.4952C42.1362 19.6505 41.0247 18.0693 38.9254 18.0693C36.8261 18.0693 35.6979 19.6505 35.6979 21.4952C35.6979 23.3399 36.8076 24.921 38.9254 24.921Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M45.0867 17.1386H46.3717V19.3515C46.9792 17.8936 48.2643 16.9629 50.1384 16.9629C52.5516 16.9629 54.1819 18.8076 54.1819 21.512C54.1819 24.2164 52.5516 26.0275 50.1384 26.0275C48.2292 26.0275 46.9626 25.0612 46.3717 23.6389V29.0496H45.0867V17.1386ZM49.7377 24.921C51.4733 24.921 52.8968 23.8314 52.8968 21.512C52.8968 19.1926 51.4733 18.0693 49.7377 18.0693C48.0021 18.0693 46.4234 19.2113 46.4234 21.512C46.4234 23.8127 48.0206 24.921 49.7377 24.921Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M59.6306 16.9629C62.0955 16.9629 63.7443 18.5272 63.7443 21.1961V21.7232H56.8204C56.8721 23.5511 57.7916 24.9547 59.7007 24.9547C61.2794 24.9547 62.2007 24.1118 62.5128 22.811H63.7794C63.4157 24.3566 62.3743 26.0256 59.7358 26.0256C56.9423 26.0256 55.5889 23.9174 55.5889 21.37C55.5889 18.559 57.2026 16.961 59.6324 16.961L59.6306 16.9629ZM62.4758 20.7046C62.354 19.0001 61.2258 18.0338 59.6121 18.0338C58.1018 18.0338 56.9736 19.0169 56.8352 20.7046H62.4758Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M65.5483 17.1386H66.8334V19.5626C67.3024 17.946 68.5173 16.9629 70.408 16.9629C72.2987 16.9629 73.497 18.2282 73.497 20.5121V25.8518H72.1953V20.6168C72.1953 18.8076 71.5011 18.0693 69.9741 18.0693C68.0299 18.0693 66.8334 19.7215 66.8334 22.0577V25.8518H65.5483V17.1386Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M75.6665 13.466H80.1624C81.4346 13.466 82.5572 13.6249 83.5302 13.9408C84.5014 14.2566 85.2972 14.8715 85.9157 15.7854C86.5343 16.6994 86.8445 17.9871 86.8445 19.6505C86.8445 21.3139 86.5343 22.6203 85.9157 23.5324C85.2972 24.4463 84.5014 25.0612 83.5302 25.3771C82.5572 25.6929 81.4364 25.8518 80.1624 25.8518H75.6665V13.466ZM80.1624 23.3754C81.0413 23.3754 81.7448 23.3044 82.271 23.1642C82.7972 23.024 83.2256 22.6876 83.5561 22.1531C83.8866 21.6204 84.0509 20.785 84.0509 19.6505C84.0509 18.516 83.8829 17.6955 83.5487 17.1554C83.2126 16.6171 82.7843 16.277 82.2636 16.1368C81.7429 15.9966 81.0432 15.9256 80.1643 15.9256H78.4453V23.3754H80.1643H80.1624Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M88.6575 18.9384C89.0453 18.2525 89.5826 17.7292 90.2713 17.3666C90.96 17.004 91.7502 16.8227 92.6402 16.8227C94.0988 16.8227 95.238 17.2152 96.0597 18.0002C96.8813 18.7852 97.2912 19.8692 97.2912 21.2503V21.9886H90.731C90.7772 22.6203 90.9747 23.1156 91.3219 23.4726C91.669 23.8295 92.1435 24.009 92.7454 24.009C93.2661 24.009 93.6963 23.8987 94.0379 23.6744C94.3795 23.452 94.5955 23.1362 94.6897 22.725H97.3097C97.1712 23.7081 96.704 24.5061 95.912 25.1135C95.1199 25.7228 94.0693 26.0275 92.762 26.0275C91.8019 26.0275 90.9692 25.8312 90.262 25.4387C89.5567 25.0463 89.0157 24.4986 88.6391 23.7959C88.2624 23.0932 88.0759 22.2858 88.0759 21.3718C88.0759 20.4579 88.2698 19.6243 88.6575 18.9384ZM94.7229 20.5457C94.6767 19.9832 94.4718 19.5496 94.1062 19.2449C93.7425 18.9403 93.2698 18.7889 92.6919 18.7889C92.114 18.7889 91.6653 18.9422 91.3126 19.2449C90.96 19.5496 90.7606 19.9832 90.7144 20.5457H94.7229Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M99.6342 25.1659C98.8698 24.5921 98.4875 23.7492 98.4875 22.6352H101.126C101.126 23.1399 101.283 23.5118 101.595 23.751C101.907 23.9921 102.387 24.1117 103.035 24.1117C103.51 24.1117 103.853 24.0501 104.067 23.9267C104.281 23.8034 104.389 23.6071 104.389 23.338C104.389 23.1511 104.326 22.9978 104.198 22.882C104.071 22.7642 103.868 22.6652 103.591 22.5829L100.917 21.8447C100.327 21.6933 99.8188 21.4204 99.3904 21.0279C98.9621 20.6354 98.7479 20.0934 98.7479 19.4019C98.7479 18.5702 99.0858 17.9329 99.7634 17.4862C100.441 17.0414 101.386 16.819 102.601 16.819C103.944 16.819 104.99 17.0918 105.742 17.6357C106.493 18.1796 106.87 18.9683 106.87 19.9981H104.232C104.232 19.144 103.694 18.716 102.618 18.716C102.236 18.716 101.935 18.7814 101.715 18.9085C101.495 19.0375 101.385 19.2075 101.385 19.4187C101.385 19.7813 101.702 20.0448 102.339 20.2093L104.387 20.7195C105.184 20.9195 105.825 21.226 106.305 21.6428C106.785 22.0577 107.025 22.6409 107.025 23.3903C107.025 24.2463 106.687 24.8986 106.01 25.349C105.332 25.7994 104.329 26.0256 102.998 26.0256C101.517 26.0256 100.395 25.7378 99.6305 25.164L99.6342 25.1659Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M108.486 12.939H111.124V20.4224L114.612 16.9965H117.84L113.484 21.3008L117.823 25.8518H114.612L111.124 22.1269V25.8518H108.486V12.939Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB |
@@ -5,7 +5,7 @@ import { AppWrapper } from '@/tests/utils';
|
||||
|
||||
import Page from '../pages';
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
jest.mock('next/router', () => ({
|
||||
useRouter() {
|
||||
return {
|
||||
push: jest.fn(),
|
||||
@@ -13,6 +13,12 @@ jest.mock('next/navigation', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@sentry/nextjs', () => ({
|
||||
captureException: jest.fn(),
|
||||
captureMessage: jest.fn(),
|
||||
setUser: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Page', () => {
|
||||
it('checks Page rendering', () => {
|
||||
render(<Page />, { wrapper: AppWrapper });
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export const backendUrl = () =>
|
||||
process.env.NEXT_PUBLIC_API_ORIGIN ||
|
||||
(typeof window !== 'undefined' ? window.location.origin : '');
|
||||
|
||||
export const baseApiUrl = (apiVersion: string = '1.0') =>
|
||||
`${backendUrl()}/api/v${apiVersion}/`;
|
||||
@@ -1,5 +1,4 @@
|
||||
import { baseApiUrl } from '@/core';
|
||||
|
||||
import { baseApiUrl } from './config';
|
||||
import { getCSRFToken } from './utils';
|
||||
|
||||
interface FetchAPIInit extends RequestInit {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './APIError';
|
||||
export * from './config';
|
||||
export * from './fetchApi';
|
||||
export * from './helpers';
|
||||
export * from './types';
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,30 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url('OpenSans-Regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url('OpenSans-Italic.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url('OpenSans-Bold.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url('OpenSans-Semibold.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url('OpenSans-Bold.woff2') format('woff2');
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentPropsWithRef, ReactHTML } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { CSSProperties } from 'styled-components/dist/types';
|
||||
import { CSSProperties, RuleSet } from 'styled-components/dist/types';
|
||||
|
||||
import {
|
||||
MarginPadding,
|
||||
@@ -15,7 +15,7 @@ export interface BoxProps {
|
||||
$align?: CSSProperties['alignItems'];
|
||||
$background?: CSSProperties['background'];
|
||||
$color?: CSSProperties['color'];
|
||||
$css?: string;
|
||||
$css?: string | RuleSet<object>;
|
||||
$direction?: CSSProperties['flexDirection'];
|
||||
$display?: CSSProperties['display'];
|
||||
$effect?: 'show' | 'hide';
|
||||
@@ -73,7 +73,7 @@ export const Box = styled('div')<BoxProps>`
|
||||
${({ $transition }) => $transition && `transition: ${$transition};`}
|
||||
${({ $width }) => $width && `width: ${$width};`}
|
||||
${({ $wrap }) => $wrap && `flex-wrap: ${$wrap};`}
|
||||
${({ $css }) => $css && `${$css};`}
|
||||
${({ $css }) => $css && (typeof $css === 'string' ? `${$css};` : $css)}
|
||||
${({ $zIndex }) => $zIndex && `z-index: ${$zIndex};`}
|
||||
${({ $effect }) => {
|
||||
let effect;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ComponentPropsWithRef, forwardRef } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxType } from './Box';
|
||||
|
||||
@@ -26,7 +27,7 @@ const BoxButton = forwardRef<HTMLDivElement, BoxType>(
|
||||
$background="none"
|
||||
$margin="none"
|
||||
$padding="none"
|
||||
$css={`
|
||||
$css={css`
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
@@ -15,7 +16,7 @@ export const Card = ({
|
||||
<Box
|
||||
$background="white"
|
||||
$radius="4px"
|
||||
$css={`
|
||||
$css={css`
|
||||
box-shadow: 2px 2px 5px ${colorsTokens()['greyscale-300']};
|
||||
border: 1px solid ${colorsTokens()['card-border']};
|
||||
${$css}
|
||||
|
||||
@@ -7,6 +7,7 @@ import '@/i18n/initI18n';
|
||||
import { useResponsiveStore } from '@/stores/';
|
||||
|
||||
import { Auth } from './auth/';
|
||||
import { ConfigProvider } from './config/';
|
||||
|
||||
/**
|
||||
* QueryClient:
|
||||
@@ -39,7 +40,9 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CunninghamProvider theme={theme}>
|
||||
<Auth>{children}</Auth>
|
||||
<ConfigProvider>
|
||||
<Auth>{children}</Auth>
|
||||
</ConfigProvider>
|
||||
</CunninghamProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Crisp } from 'crisp-sdk-web';
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import { useAuthStore } from '../useAuthStore';
|
||||
|
||||
jest.mock('crisp-sdk-web', () => ({
|
||||
...jest.requireActual('crisp-sdk-web'),
|
||||
Crisp: {
|
||||
isCrispInjected: jest.fn().mockReturnValue(true),
|
||||
setTokenId: jest.fn(),
|
||||
user: {
|
||||
setEmail: jest.fn(),
|
||||
},
|
||||
session: {
|
||||
reset: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useAuthStore', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it('checks support session is terminated when logout', () => {
|
||||
window.$crisp = true;
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
...window.location,
|
||||
replace: jest.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
useAuthStore.getState().logout();
|
||||
|
||||
expect(Crisp.session.reset).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { baseApiUrl } from '@/core/conf';
|
||||
import { baseApiUrl } from '@/api';
|
||||
import { terminateCrispSession } from '@/services';
|
||||
|
||||
import { User, getMe } from './api';
|
||||
import { PATH_AUTH_LOCAL_STORAGE } from './conf';
|
||||
@@ -42,6 +43,7 @@ export const useAuthStore = create<AuthStore>((set, get) => ({
|
||||
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
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
export const mediaUrl = () =>
|
||||
process.env.NEXT_PUBLIC_MEDIA_URL ||
|
||||
(typeof window !== 'undefined' ? window.location.origin : '');
|
||||
|
||||
export const backendUrl = () =>
|
||||
process.env.NEXT_PUBLIC_API_ORIGIN ||
|
||||
(typeof window !== 'undefined' ? window.location.origin : '');
|
||||
|
||||
export const baseApiUrl = (apiVersion: string = '1.0') =>
|
||||
`${backendUrl()}/api/v${apiVersion}/`;
|
||||
|
||||
export const providerUrl = (docId: string) => {
|
||||
const base =
|
||||
process.env.NEXT_PUBLIC_Y_PROVIDER_URL ||
|
||||
(typeof window !== 'undefined' ? `wss://${window.location.host}/ws` : '');
|
||||
|
||||
return `${base}/${docId}`;
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { PropsWithChildren, useEffect } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { configureCrispSession } from '@/services';
|
||||
import { useSentryStore } from '@/stores/useSentryStore';
|
||||
|
||||
import { useConfig } from './api/useConfig';
|
||||
|
||||
export const ConfigProvider = ({ children }: PropsWithChildren) => {
|
||||
const { data: conf } = useConfig();
|
||||
const { setSentry } = useSentryStore();
|
||||
const { setTheme } = useCunninghamTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (!conf?.SENTRY_DSN) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSentry(conf.SENTRY_DSN, conf.ENVIRONMENT);
|
||||
}, [conf?.SENTRY_DSN, conf?.ENVIRONMENT, setSentry]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!conf?.FRONTEND_THEME) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTheme(conf.FRONTEND_THEME);
|
||||
}, [conf?.FRONTEND_THEME, setTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!conf?.CRISP_WEBSITE_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
configureCrispSession(conf.CRISP_WEBSITE_ID);
|
||||
}, [conf?.CRISP_WEBSITE_ID]);
|
||||
|
||||
if (!conf) {
|
||||
return (
|
||||
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './useConfig';
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { Theme } from '@/cunningham/';
|
||||
|
||||
interface ConfigResponse {
|
||||
LANGUAGES: [string, string][];
|
||||
LANGUAGE_CODE: string;
|
||||
ENVIRONMENT: string;
|
||||
COLLABORATION_SERVER_URL?: string;
|
||||
CRISP_WEBSITE_ID?: string;
|
||||
FRONTEND_THEME?: Theme;
|
||||
MEDIA_BASE_URL?: string;
|
||||
SENTRY_DSN?: string;
|
||||
}
|
||||
|
||||
export const getConfig = async (): Promise<ConfigResponse> => {
|
||||
const response = await fetchAPI(`config/`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError('Failed to get the doc', await errorCauses(response));
|
||||
}
|
||||
|
||||
return response.json() as Promise<ConfigResponse>;
|
||||
};
|
||||
|
||||
export const KEY_CONFIG = 'config';
|
||||
|
||||
export function useConfig() {
|
||||
return useQuery<ConfigResponse, APIError, ConfigResponse>({
|
||||
queryKey: [KEY_CONFIG],
|
||||
queryFn: () => getConfig(),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './useMediaUrl';
|
||||
export * from './useCollaborationUrl';
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useConfig } from '../api';
|
||||
|
||||
export const useCollaborationUrl = (room?: string) => {
|
||||
const { data: conf } = useConfig();
|
||||
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
|
||||
const base =
|
||||
conf?.COLLABORATION_SERVER_URL ||
|
||||
(typeof window !== 'undefined' ? `wss://${window.location.host}/ws` : '');
|
||||
|
||||
return `${base}/${room}`;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useConfig } from '../api';
|
||||
|
||||
export const useMediaUrl = () => {
|
||||
const { data: conf } = useConfig();
|
||||
|
||||
return (
|
||||
conf?.MEDIA_BASE_URL ||
|
||||
(typeof window !== 'undefined' ? window.location.origin : '')
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './api/';
|
||||
export * from './ConfigProvider';
|
||||
export * from './hooks';
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './AppProvider';
|
||||
export * from './auth';
|
||||
export * from './conf';
|
||||
export * from './config';
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import useCunninghamTheme from '../useCunninghamTheme';
|
||||
import { useCunninghamTheme } from '../useCunninghamTheme';
|
||||
|
||||
describe('<useCunninghamTheme />', () => {
|
||||
it('has the theme from NEXT_PUBLIC_THEME', () => {
|
||||
const { theme } = useCunninghamTheme.getState();
|
||||
|
||||
expect(theme).toBe('test-theme');
|
||||
});
|
||||
|
||||
it('has the dsfr logo correctly set', () => {
|
||||
const { themeTokens, setTheme } = useCunninghamTheme.getState();
|
||||
setTheme('dsfr');
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
@import url('./cunningham-tokens.css');
|
||||
@import url('./cunningham-custom-tokens.css');
|
||||
@import url('../assets/fonts/Marianne/Marianne-font.css');
|
||||
@import url('../assets/fonts/OpenSans/OpenSans-font.css');
|
||||
|
||||
.c__input,
|
||||
.c__field,
|
||||
|
||||
@@ -523,199 +523,6 @@
|
||||
--c--components--la-gauffre--activated: true;
|
||||
}
|
||||
|
||||
.cunningham-theme--open-desk {
|
||||
--c--theme--colors--card-border: #ededed;
|
||||
--c--theme--colors--primary-text: #637089;
|
||||
--c--theme--colors--primary-100: #f7f5ff;
|
||||
--c--theme--colors--primary-200: #ece7fe;
|
||||
--c--theme--colors--primary-300: #dcd2fe;
|
||||
--c--theme--colors--primary-400: #c8b9fd;
|
||||
--c--theme--colors--primary-500: #8e75fa;
|
||||
--c--theme--colors--primary-600: #7051fa;
|
||||
--c--theme--colors--primary-700: #571efa;
|
||||
--c--theme--colors--primary-800: #4519c2;
|
||||
--c--theme--colors--primary-900: #341291;
|
||||
--c--theme--colors--secondary-text: #fff;
|
||||
--c--theme--colors--secondary-100: #edfdfb;
|
||||
--c--theme--colors--secondary-200: #bff9f2;
|
||||
--c--theme--colors--secondary-300: #71efe1;
|
||||
--c--theme--colors--secondary-400: #00e6cc;
|
||||
--c--theme--colors--secondary-500: #00a896;
|
||||
--c--theme--colors--secondary-600: #008a7b;
|
||||
--c--theme--colors--secondary-700: #006c60;
|
||||
--c--theme--colors--secondary-800: #00564d;
|
||||
--c--theme--colors--secondary-900: #004039;
|
||||
--c--theme--colors--greyscale-text: #303c4b;
|
||||
--c--theme--colors--greyscale-000: #fff;
|
||||
--c--theme--colors--greyscale-100: #eeeff2;
|
||||
--c--theme--colors--greyscale-200: #d3d7de;
|
||||
--c--theme--colors--greyscale-300: #b6bcc8;
|
||||
--c--theme--colors--greyscale-400: #7c879c;
|
||||
--c--theme--colors--greyscale-500: #637089;
|
||||
--c--theme--colors--greyscale-600: #4d5b79;
|
||||
--c--theme--colors--greyscale-700: #364768;
|
||||
--c--theme--colors--greyscale-800: #203257;
|
||||
--c--theme--colors--greyscale-900: #1e1e1e;
|
||||
--c--theme--colors--success-text: #1f8d49;
|
||||
--c--theme--colors--success-100: #dffee6;
|
||||
--c--theme--colors--success-200: #b8fec9;
|
||||
--c--theme--colors--success-300: #88fdaa;
|
||||
--c--theme--colors--success-400: #3bea7e;
|
||||
--c--theme--colors--success-500: #1f8d49;
|
||||
--c--theme--colors--success-600: #18753c;
|
||||
--c--theme--colors--success-700: #204129;
|
||||
--c--theme--colors--success-800: #1e2e22;
|
||||
--c--theme--colors--success-900: #19281d;
|
||||
--c--theme--colors--info-text: #0078f3;
|
||||
--c--theme--colors--info-100: #f4f6ff;
|
||||
--c--theme--colors--info-200: #e8edff;
|
||||
--c--theme--colors--info-300: #dde5ff;
|
||||
--c--theme--colors--info-400: #bdcdff;
|
||||
--c--theme--colors--info-500: #0078f3;
|
||||
--c--theme--colors--info-600: #0063cb;
|
||||
--c--theme--colors--info-700: #f4f6ff;
|
||||
--c--theme--colors--info-800: #222a3f;
|
||||
--c--theme--colors--info-900: #1d2437;
|
||||
--c--theme--colors--warning-text: #d64d00;
|
||||
--c--theme--colors--warning-100: #fff4f3;
|
||||
--c--theme--colors--warning-200: #ffe9e6;
|
||||
--c--theme--colors--warning-300: #ffded9;
|
||||
--c--theme--colors--warning-400: #ffbeb4;
|
||||
--c--theme--colors--warning-500: #d64d00;
|
||||
--c--theme--colors--warning-600: #b34000;
|
||||
--c--theme--colors--warning-700: #5e2c21;
|
||||
--c--theme--colors--warning-800: #3e241e;
|
||||
--c--theme--colors--warning-900: #361e19;
|
||||
--c--theme--colors--danger-text: #e1000f;
|
||||
--c--theme--colors--danger-100: #fef4f4;
|
||||
--c--theme--colors--danger-200: #fee9e9;
|
||||
--c--theme--colors--danger-300: #fddede;
|
||||
--c--theme--colors--danger-400: #fcbfbf;
|
||||
--c--theme--colors--danger-500: #e1000f;
|
||||
--c--theme--colors--danger-600: #c9191e;
|
||||
--c--theme--colors--danger-700: #642727;
|
||||
--c--theme--colors--danger-800: #412121;
|
||||
--c--theme--colors--danger-900: #3a1c1c;
|
||||
--c--theme--font--families--accent: open sans;
|
||||
--c--theme--font--families--base: open sans;
|
||||
--c--theme--logo--src: /assets/logo-open-desk.svg;
|
||||
--c--theme--logo--widthHeader: 110px;
|
||||
--c--theme--logo--widthFooter: 220px;
|
||||
--c--theme--logo--alt: opendesk logo;
|
||||
--c--components--alert--border-radius: 0;
|
||||
--c--components--button--medium-height: 48px;
|
||||
--c--components--button--border-radius: 4px;
|
||||
--c--components--button--primary--background--color: var(
|
||||
--c--theme--colors--primary-700
|
||||
);
|
||||
--c--components--button--primary--background--color-hover: var(
|
||||
--c--theme--colors--primary-900
|
||||
);
|
||||
--c--components--button--primary--background--color-active: var(
|
||||
--c--theme--colors--primary-900
|
||||
);
|
||||
--c--components--button--primary--color: #fff;
|
||||
--c--components--button--primary--color-hover: #fff;
|
||||
--c--components--button--primary--color-active: #fff;
|
||||
--c--components--button--primary-text--background--color-hover: var(
|
||||
--c--theme--colors--primary-100
|
||||
);
|
||||
--c--components--button--primary-text--background--color-active: var(
|
||||
--c--theme--colors--primary-100
|
||||
);
|
||||
--c--components--button--primary-text--color-hover: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--button--secondary--background--color-hover: var(
|
||||
--c--theme--colors--primary-100
|
||||
);
|
||||
--c--components--button--secondary--background--color-active: #ededed;
|
||||
--c--components--button--secondary--border--color: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--button--secondary--border--color-hover: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--button--secondary--color: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--button--secondary--color-hover: var(
|
||||
--c--theme--colors--primary-700
|
||||
);
|
||||
--c--components--button--tertiary-text--background--color-hover: var(
|
||||
--c--theme--colors--primary-100
|
||||
);
|
||||
--c--components--button--tertiary-text--color: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--button--tertiary-text--color-hover: var(
|
||||
--c--theme--colors--primary-700
|
||||
);
|
||||
--c--components--datagrid--header--color: var(
|
||||
--c--theme--colors--primary-500
|
||||
);
|
||||
--c--components--datagrid--header--size: var(--c--theme--font--sizes--s);
|
||||
--c--components--datagrid--body--background-color: transparent;
|
||||
--c--components--datagrid--body--background-color-hover: #f4f4fd;
|
||||
--c--components--datagrid--pagination--background-color: var(
|
||||
--c--theme--colors--primary-100
|
||||
);
|
||||
--c--components--datagrid--pagination--border-color: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--datagrid--pagination--background-color-active: var(
|
||||
--c--theme--colors--primary-300
|
||||
);
|
||||
--c--components--forms-checkbox--border-radius: 0;
|
||||
--c--components--forms-checkbox--color: var(
|
||||
--c--theme--colors--greyscale-600
|
||||
);
|
||||
--c--components--forms-checkbox--text--color: var(
|
||||
--c--theme--colors--greyscale-600
|
||||
);
|
||||
--c--components--forms-checkbox--text--size: var(--c--theme--font--sizes--t);
|
||||
--c--components--forms-datepicker--border-radius: 0;
|
||||
--c--components--forms-fileuploader--border-radius: 0;
|
||||
--c--components--forms-field--color: var(--c--theme--colors--primary-600);
|
||||
--c--components--forms-input--border-radius: 4px;
|
||||
--c--components--forms-input--background-color: #fff;
|
||||
--c--components--forms-input--border-color: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--forms-input--box-shadow-color: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--forms-input--value-color: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--forms-input--font-size: 14px;
|
||||
--c--components--forms-labelledbox--label-color--big: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--forms-select--item-font-size: 14px;
|
||||
--c--components--forms-select--border-radius: 4px;
|
||||
--c--components--forms-select--border-radius-hover: 4px;
|
||||
--c--components--forms-select--background-color: #fff;
|
||||
--c--components--forms-select--border-color: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--forms-select--border-color-hover: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--forms-select--box-shadow-color: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--forms-radio--accent-color: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--forms-switch--handle-border-radius: 2px;
|
||||
--c--components--forms-switch--rail-border-radius: 4px;
|
||||
--c--components--forms-switch--accent-color: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--forms-textarea--border-radius: 0;
|
||||
}
|
||||
|
||||
.clr-secondary-text {
|
||||
color: var(--c--theme--colors--secondary-text);
|
||||
}
|
||||
|
||||
@@ -516,188 +516,5 @@ export const tokens = {
|
||||
'la-gauffre': { activated: true },
|
||||
},
|
||||
},
|
||||
'open-desk': {
|
||||
theme: {
|
||||
colors: {
|
||||
'card-border': '#ededed',
|
||||
'primary-text': '#637089',
|
||||
'primary-100': '#F7F5FF',
|
||||
'primary-200': '#ECE7FE',
|
||||
'primary-300': '#DCD2FE',
|
||||
'primary-400': '#C8B9FD',
|
||||
'primary-500': '#8E75FA',
|
||||
'primary-600': '#7051FA',
|
||||
'primary-700': '#571EFA',
|
||||
'primary-800': '#4519C2',
|
||||
'primary-900': '#341291',
|
||||
'secondary-text': '#FFFFFF',
|
||||
'secondary-100': '#EDFDFB',
|
||||
'secondary-200': '#BFF9F2',
|
||||
'secondary-300': '#71EFE1',
|
||||
'secondary-400': '#00E6CC',
|
||||
'secondary-500': '#00A896',
|
||||
'secondary-600': '#008A7B',
|
||||
'secondary-700': '#006C60',
|
||||
'secondary-800': '#00564D',
|
||||
'secondary-900': '#004039',
|
||||
'greyscale-text': '#303C4B',
|
||||
'greyscale-000': '#ffffff',
|
||||
'greyscale-100': '#EEEFF2',
|
||||
'greyscale-200': '#D3D7DE',
|
||||
'greyscale-300': '#B6BCC8',
|
||||
'greyscale-400': '#7C879C',
|
||||
'greyscale-500': '#637089',
|
||||
'greyscale-600': '#4D5B79',
|
||||
'greyscale-700': '#364768',
|
||||
'greyscale-800': '#203257',
|
||||
'greyscale-900': '#1e1e1e',
|
||||
'success-text': '#1f8d49',
|
||||
'success-100': '#dffee6',
|
||||
'success-200': '#b8fec9',
|
||||
'success-300': '#88fdaa',
|
||||
'success-400': '#3bea7e',
|
||||
'success-500': '#1f8d49',
|
||||
'success-600': '#18753c',
|
||||
'success-700': '#204129',
|
||||
'success-800': '#1e2e22',
|
||||
'success-900': '#19281d',
|
||||
'info-text': '#0078f3',
|
||||
'info-100': '#f4f6ff',
|
||||
'info-200': '#e8edff',
|
||||
'info-300': '#dde5ff',
|
||||
'info-400': '#bdcdff',
|
||||
'info-500': '#0078f3',
|
||||
'info-600': '#0063cb',
|
||||
'info-700': '#f4f6ff',
|
||||
'info-800': '#222a3f',
|
||||
'info-900': '#1d2437',
|
||||
'warning-text': '#d64d00',
|
||||
'warning-100': '#fff4f3',
|
||||
'warning-200': '#ffe9e6',
|
||||
'warning-300': '#ffded9',
|
||||
'warning-400': '#ffbeb4',
|
||||
'warning-500': '#d64d00',
|
||||
'warning-600': '#b34000',
|
||||
'warning-700': '#5e2c21',
|
||||
'warning-800': '#3e241e',
|
||||
'warning-900': '#361e19',
|
||||
'danger-text': '#e1000f',
|
||||
'danger-100': '#fef4f4',
|
||||
'danger-200': '#fee9e9',
|
||||
'danger-300': '#fddede',
|
||||
'danger-400': '#fcbfbf',
|
||||
'danger-500': '#e1000f',
|
||||
'danger-600': '#c9191e',
|
||||
'danger-700': '#642727',
|
||||
'danger-800': '#412121',
|
||||
'danger-900': '#3a1c1c',
|
||||
},
|
||||
font: { families: { accent: 'Open Sans', base: 'Open Sans' } },
|
||||
logo: {
|
||||
src: '/assets/logo-open-desk.svg',
|
||||
widthHeader: '110px',
|
||||
widthFooter: '220px',
|
||||
alt: 'OpenDesk Logo',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
alert: { 'border-radius': '0' },
|
||||
button: {
|
||||
'medium-height': '48px',
|
||||
'border-radius': '4px',
|
||||
primary: {
|
||||
background: {
|
||||
color: 'var(--c--theme--colors--primary-700)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-900)',
|
||||
'color-active': 'var(--c--theme--colors--primary-900)',
|
||||
},
|
||||
color: '#ffffff',
|
||||
'color-hover': '#ffffff',
|
||||
'color-active': '#ffffff',
|
||||
},
|
||||
'primary-text': {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
'color-active': 'var(--c--theme--colors--primary-100)',
|
||||
},
|
||||
'color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
},
|
||||
secondary: {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
'color-active': '#EDEDED',
|
||||
},
|
||||
border: {
|
||||
color: 'var(--c--theme--colors--primary-600)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-600)',
|
||||
},
|
||||
color: 'var(--c--theme--colors--primary-600)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-700)',
|
||||
},
|
||||
'tertiary-text': {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
},
|
||||
color: 'var(--c--theme--colors--primary-600)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-700)',
|
||||
},
|
||||
},
|
||||
datagrid: {
|
||||
header: {
|
||||
color: 'var(--c--theme--colors--primary-500)',
|
||||
size: 'var(--c--theme--font--sizes--s)',
|
||||
},
|
||||
body: {
|
||||
'background-color': 'transparent',
|
||||
'background-color-hover': '#F4F4FD',
|
||||
},
|
||||
pagination: {
|
||||
'background-color': 'var(--c--theme--colors--primary-100)',
|
||||
'border-color': 'var(--c--theme--colors--primary-600)',
|
||||
'background-color-active': 'var(--c--theme--colors--primary-300)',
|
||||
},
|
||||
},
|
||||
'forms-checkbox': {
|
||||
'border-radius': '0',
|
||||
color: 'var(--c--theme--colors--greyscale-600)',
|
||||
text: {
|
||||
color: 'var(--c--theme--colors--greyscale-600)',
|
||||
size: 'var(--c--theme--font--sizes--t)',
|
||||
},
|
||||
},
|
||||
'forms-datepicker': { 'border-radius': '0' },
|
||||
'forms-fileuploader': { 'border-radius': '0' },
|
||||
'forms-field': { color: 'var(--c--theme--colors--primary-600)' },
|
||||
'forms-input': {
|
||||
'border-radius': '4px',
|
||||
'background-color': '#ffffff',
|
||||
'border-color': 'var(--c--theme--colors--primary-600)',
|
||||
'box-shadow-color': 'var(--c--theme--colors--primary-600)',
|
||||
'value-color': 'var(--c--theme--colors--primary-600)',
|
||||
'font-size': '14px',
|
||||
},
|
||||
'forms-labelledbox': {
|
||||
'label-color': { big: 'var(--c--theme--colors--primary-600)' },
|
||||
},
|
||||
'forms-select': {
|
||||
'item-font-size': '14px',
|
||||
'border-radius': '4px',
|
||||
'border-radius-hover': '4px',
|
||||
'background-color': '#ffffff',
|
||||
'border-color': 'var(--c--theme--colors--primary-600)',
|
||||
'border-color-hover': 'var(--c--theme--colors--primary-600)',
|
||||
'box-shadow-color': 'var(--c--theme--colors--primary-600)',
|
||||
},
|
||||
'forms-radio': {
|
||||
'accent-color': 'var(--c--theme--colors--primary-600)',
|
||||
},
|
||||
'forms-switch': {
|
||||
'handle-border-radius': '2px',
|
||||
'rail-border-radius': '4px',
|
||||
'accent-color': 'var(--c--theme--colors--primary-600)',
|
||||
},
|
||||
'forms-textarea': { 'border-radius': '0' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
import { tokens } from './cunningham-tokens';
|
||||
import useCunninghamTheme from './useCunninghamTheme';
|
||||
|
||||
export { tokens, useCunninghamTheme };
|
||||
export * from './cunningham-tokens';
|
||||
export * from './useCunninghamTheme';
|
||||
|
||||
@@ -6,22 +6,25 @@ import { tokens } from './cunningham-tokens';
|
||||
type Tokens = typeof tokens.themes.default & Partial<typeof tokens.themes.dsfr>;
|
||||
type ColorsTokens = Tokens['theme']['colors'];
|
||||
type ComponentTokens = Tokens['components'];
|
||||
type Theme = 'default' | 'dsfr';
|
||||
export type Theme = keyof typeof tokens.themes;
|
||||
|
||||
interface AuthStore {
|
||||
theme: Theme;
|
||||
theme: string;
|
||||
setTheme: (theme: Theme) => void;
|
||||
themeTokens: () => Partial<Tokens['theme']>;
|
||||
colorsTokens: () => Partial<ColorsTokens>;
|
||||
componentTokens: () => ComponentTokens;
|
||||
}
|
||||
|
||||
const useCunninghamTheme = create<AuthStore>((set, get) => {
|
||||
export const useCunninghamTheme = create<AuthStore>((set, get) => {
|
||||
const currentTheme = () =>
|
||||
merge(tokens.themes['default'], tokens.themes[get().theme]) as Tokens;
|
||||
merge(
|
||||
tokens.themes['default'],
|
||||
tokens.themes[get().theme as keyof typeof tokens.themes],
|
||||
) as Tokens;
|
||||
|
||||
return {
|
||||
theme: (process.env.NEXT_PUBLIC_THEME as Theme) || 'dsfr',
|
||||
theme: 'dsfr',
|
||||
themeTokens: () => currentTheme().theme,
|
||||
colorsTokens: () => currentTheme().theme.colors,
|
||||
componentTokens: () => currentTheme().components,
|
||||
@@ -30,5 +33,3 @@ const useCunninghamTheme = create<AuthStore>((set, get) => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default useCunninghamTheme;
|
||||
|
||||
@@ -20,9 +20,6 @@ declare module '*.svg?url' {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NEXT_PUBLIC_API_ORIGIN?: string;
|
||||
NEXT_PUBLIC_MEDIA_URL?: string;
|
||||
NEXT_PUBLIC_Y_PROVIDER_URL?: string;
|
||||
NEXT_PUBLIC_SW_DEACTIVATED?: string;
|
||||
NEXT_PUBLIC_THEME?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,13 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { isAPIError } from '@/api';
|
||||
import { Box, Text } from '@/components';
|
||||
import { useDocOptions } from '@/features/docs/doc-management/';
|
||||
import { useDocOptions, useDocStore } from '@/features/docs/doc-management/';
|
||||
|
||||
import {
|
||||
AITransformActions,
|
||||
useDocAITransform,
|
||||
useDocAITranslate,
|
||||
} from '../api/';
|
||||
import { useDocStore } from '../stores';
|
||||
|
||||
type LanguageTranslate = {
|
||||
value: string;
|
||||
|
||||
+49
-63
@@ -1,21 +1,19 @@
|
||||
import { locales } from '@blocknote/core';
|
||||
import { Dictionary, locales } from '@blocknote/core';
|
||||
import '@blocknote/core/fonts/inter.css';
|
||||
import { BlockNoteView } from '@blocknote/mantine';
|
||||
import '@blocknote/mantine/style.css';
|
||||
import { useCreateBlockNote } from '@blocknote/react';
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, TextErrors } from '@/components';
|
||||
import { mediaUrl } from '@/core';
|
||||
import { useAuthStore } from '@/core/auth';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import { Version } from '@/features/docs/doc-versioning/';
|
||||
|
||||
import { useCreateDocAttachment } from '../api/useCreateDocUpload';
|
||||
import { useUploadFile } from '../hook';
|
||||
import useSaveDoc from '../hook/useSaveDoc';
|
||||
import { useDocStore, useHeadingStore } from '../stores';
|
||||
import { useEditorStore, useHeadingStore } from '../stores';
|
||||
import { randomColor } from '../utils';
|
||||
|
||||
import { BlockNoteToolbar } from './BlockNoteToolbar';
|
||||
@@ -28,9 +26,6 @@ const cssEditor = (readonly: boolean) => `
|
||||
padding-right: 30px;
|
||||
${readonly && `padding-left: 30px;`}
|
||||
};
|
||||
& .collaboration-cursor__caret.ProseMirror-widget{
|
||||
word-wrap: initial;
|
||||
}
|
||||
& .bn-inline-content code {
|
||||
background-color: gainsboro;
|
||||
padding: 2px;
|
||||
@@ -68,70 +63,32 @@ const cssEditor = (readonly: boolean) => `
|
||||
`;
|
||||
|
||||
interface BlockNoteEditorProps {
|
||||
doc: Doc;
|
||||
version?: Version;
|
||||
}
|
||||
|
||||
export const BlockNoteEditor = ({ doc, version }: BlockNoteEditorProps) => {
|
||||
const { createProvider, docsStore } = useDocStore();
|
||||
const storeId = version?.id || doc.id;
|
||||
const initialContent = version?.content || doc.content;
|
||||
const provider = docsStore?.[storeId]?.provider;
|
||||
|
||||
useEffect(() => {
|
||||
if (!provider || provider.document.guid !== storeId) {
|
||||
createProvider(storeId, initialContent);
|
||||
}
|
||||
}, [createProvider, initialContent, provider, storeId]);
|
||||
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <BlockNoteContent doc={doc} provider={provider} storeId={storeId} />;
|
||||
};
|
||||
|
||||
interface BlockNoteContentProps {
|
||||
doc: Doc;
|
||||
provider: HocuspocusProvider;
|
||||
storeId: string;
|
||||
}
|
||||
|
||||
export const BlockNoteContent = ({
|
||||
export const BlockNoteEditor = ({
|
||||
doc,
|
||||
provider,
|
||||
storeId,
|
||||
}: BlockNoteContentProps) => {
|
||||
}: BlockNoteEditorProps) => {
|
||||
const isVersion = doc.id !== storeId;
|
||||
const { userData } = useAuthStore();
|
||||
const { setStore, docsStore } = useDocStore();
|
||||
|
||||
const { setEditor } = useEditorStore();
|
||||
const { t } = useTranslation();
|
||||
const readOnly = !doc.abilities.partial_update || isVersion;
|
||||
useSaveDoc(doc.id, provider.document, !readOnly);
|
||||
const storedEditor = docsStore?.[storeId]?.editor;
|
||||
const {
|
||||
mutateAsync: createDocAttachment,
|
||||
isError: isErrorAttachment,
|
||||
error: errorAttachment,
|
||||
} = useCreateDocAttachment();
|
||||
const { setHeadings, resetHeadings } = useHeadingStore();
|
||||
const { i18n } = useTranslation();
|
||||
const lang = i18n.language;
|
||||
|
||||
const uploadFile = useCallback(
|
||||
async (file: File) => {
|
||||
const body = new FormData();
|
||||
body.append('file', file);
|
||||
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
|
||||
|
||||
const ret = await createDocAttachment({
|
||||
docId: doc.id,
|
||||
body,
|
||||
});
|
||||
|
||||
return `${mediaUrl()}${ret.file}`;
|
||||
},
|
||||
[createDocAttachment, doc.id],
|
||||
);
|
||||
const collabName =
|
||||
userData?.full_name ||
|
||||
userData?.email ||
|
||||
(readOnly ? 'Reader' : t('Anonymous'));
|
||||
|
||||
const editor = useCreateBlockNote(
|
||||
{
|
||||
@@ -139,19 +96,48 @@ export const BlockNoteContent = ({
|
||||
provider,
|
||||
fragment: provider.document.getXmlFragment('document-store'),
|
||||
user: {
|
||||
name: userData?.email || 'Anonymous',
|
||||
name: collabName,
|
||||
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
|
||||
*/
|
||||
renderCursor: (user: { color: string; name: string }) => {
|
||||
const cursor = document.createElement('span');
|
||||
|
||||
if (user.name === 'Reader') {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
cursor.classList.add('collaboration-cursor__caret');
|
||||
cursor.setAttribute('style', `border-color: ${user.color}`);
|
||||
|
||||
const label = document.createElement('span');
|
||||
|
||||
label.classList.add('collaboration-cursor__label');
|
||||
label.setAttribute('style', `background-color: ${user.color}`);
|
||||
label.insertBefore(document.createTextNode(user.name), null);
|
||||
|
||||
cursor.insertBefore(label, null);
|
||||
|
||||
return cursor;
|
||||
},
|
||||
},
|
||||
dictionary: locales[lang as keyof typeof locales],
|
||||
dictionary: locales[lang as keyof typeof locales] as Dictionary,
|
||||
uploadFile,
|
||||
},
|
||||
[provider, uploadFile, userData?.email, lang],
|
||||
[collabName, lang, provider, uploadFile],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setStore(storeId, { editor });
|
||||
}, [setStore, storeId, editor]);
|
||||
setEditor(editor);
|
||||
|
||||
return () => {
|
||||
setEditor(undefined);
|
||||
};
|
||||
}, [setEditor, editor]);
|
||||
|
||||
useEffect(() => {
|
||||
setHeadings(editor);
|
||||
@@ -167,7 +153,7 @@ export const BlockNoteContent = ({
|
||||
|
||||
return (
|
||||
<Box $css={cssEditor(readOnly)}>
|
||||
{isErrorAttachment && (
|
||||
{errorAttachment && (
|
||||
<Box $margin={{ bottom: 'big' }}>
|
||||
<TextErrors
|
||||
causes={errorAttachment.cause}
|
||||
@@ -178,7 +164,7 @@ export const BlockNoteContent = ({
|
||||
)}
|
||||
|
||||
<BlockNoteView
|
||||
editor={storedEditor ?? editor}
|
||||
editor={editor}
|
||||
formattingToolbar={false}
|
||||
editable={!readOnly}
|
||||
theme="light"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Alert, Loader, VariantType } from '@openfun/cunningham-react';
|
||||
import { useRouter as useNavigate } from 'next/navigation';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Card, Text, TextErrors } from '@/components';
|
||||
import { useCollaborationUrl } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { DocHeader } from '@/features/docs/doc-header';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import { Doc, useDocStore } from '@/features/docs/doc-management';
|
||||
import { Versions, useDocVersion } from '@/features/docs/doc-versioning/';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
@@ -32,6 +32,13 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const { providers } = useDocStore();
|
||||
const provider = providers?.[doc.id];
|
||||
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocHeader doc={doc} versionId={versionId as Versions['version_id']} />
|
||||
@@ -66,7 +73,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||
{isVersion ? (
|
||||
<DocVersionEditor doc={doc} versionId={versionId} />
|
||||
) : (
|
||||
<BlockNoteEditor doc={doc} />
|
||||
<BlockNoteEditor doc={doc} storeId={doc.id} provider={provider} />
|
||||
)}
|
||||
{!isMobile && <IconOpenPanelEditor headings={headings} />}
|
||||
</Card>
|
||||
@@ -91,12 +98,25 @@ export const DocVersionEditor = ({ doc, versionId }: DocVersionEditorProps) => {
|
||||
docId: doc.id,
|
||||
versionId,
|
||||
});
|
||||
const { createProvider, providers } = useDocStore();
|
||||
const collaborationUrl = useCollaborationUrl(versionId);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { replace } = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!version?.id || !collaborationUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = providers?.[version.id];
|
||||
if (!provider || provider.document.guid !== version.id) {
|
||||
createProvider(collaborationUrl, version.id, version.content);
|
||||
}
|
||||
}, [createProvider, providers, version, collaborationUrl]);
|
||||
|
||||
if (isError && error) {
|
||||
if (error.status === 404) {
|
||||
navigate.replace(`/404`);
|
||||
void replace(`/404`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -124,5 +144,11 @@ export const DocVersionEditor = ({ doc, versionId }: DocVersionEditorProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
return <BlockNoteEditor doc={doc} version={version} />;
|
||||
const provider = providers?.[version.id];
|
||||
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <BlockNoteEditor doc={doc} storeId={version.id} provider={provider} />;
|
||||
};
|
||||
|
||||
@@ -127,9 +127,7 @@ export const PanelEditor = ({
|
||||
</BoxButton>
|
||||
)}
|
||||
</Box>
|
||||
{isPanelTableContentOpen && (
|
||||
<TableContent doc={doc} headings={headings} />
|
||||
)}
|
||||
{isPanelTableContentOpen && <TableContent headings={headings} />}
|
||||
{!isPanelTableContentOpen && doc.abilities.versions_list && (
|
||||
<VersionList doc={doc} />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './useSaveDoc';
|
||||
export * from './useUploadFile';
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useMediaUrl } from '@/core/config';
|
||||
|
||||
import { useCreateDocAttachment } from '../api';
|
||||
|
||||
export const useUploadFile = (docId: string) => {
|
||||
const mediaUrl = useMediaUrl();
|
||||
const {
|
||||
mutateAsync: createDocAttachment,
|
||||
isError: isErrorAttachment,
|
||||
error: errorAttachment,
|
||||
} = useCreateDocAttachment();
|
||||
|
||||
const uploadFile = useCallback(
|
||||
async (file: File) => {
|
||||
const body = new FormData();
|
||||
body.append('file', file);
|
||||
|
||||
const ret = await createDocAttachment({
|
||||
docId,
|
||||
body,
|
||||
});
|
||||
|
||||
return `${mediaUrl}${ret.file}`;
|
||||
},
|
||||
[createDocAttachment, docId, mediaUrl],
|
||||
);
|
||||
|
||||
return {
|
||||
uploadFile,
|
||||
isErrorAttachment,
|
||||
errorAttachment,
|
||||
};
|
||||
};
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './useDocStore';
|
||||
export * from './useEditorStore';
|
||||
export * from './useHeadingStore';
|
||||
export * from './usePanelEditorStore';
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import * as Y from 'yjs';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { providerUrl } from '@/core';
|
||||
import { Base64, Doc } from '@/features/docs/doc-management';
|
||||
|
||||
import { blocksToYDoc } from '../utils';
|
||||
|
||||
interface DocStore {
|
||||
provider: HocuspocusProvider;
|
||||
editor?: BlockNoteEditor;
|
||||
}
|
||||
|
||||
export interface UseDocStore {
|
||||
currentDoc?: Doc;
|
||||
docsStore: {
|
||||
[storeId: string]: DocStore;
|
||||
};
|
||||
createProvider: (storeId: string, initialDoc: Base64) => HocuspocusProvider;
|
||||
setStore: (storeId: string, props: Partial<DocStore>) => void;
|
||||
setCurrentDoc: (doc: Doc | undefined) => void;
|
||||
}
|
||||
|
||||
export const useDocStore = create<UseDocStore>((set, get) => ({
|
||||
currentDoc: undefined,
|
||||
docsStore: {},
|
||||
createProvider: (storeId: string, initialDoc: Base64) => {
|
||||
const doc = new Y.Doc({
|
||||
guid: storeId,
|
||||
});
|
||||
|
||||
if (initialDoc) {
|
||||
Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64'));
|
||||
} else {
|
||||
const initialDocContent = [
|
||||
{
|
||||
type: 'heading',
|
||||
content: '',
|
||||
},
|
||||
];
|
||||
|
||||
blocksToYDoc(initialDocContent, doc);
|
||||
}
|
||||
|
||||
const provider = new HocuspocusProvider({
|
||||
url: providerUrl(storeId),
|
||||
name: storeId,
|
||||
document: doc,
|
||||
});
|
||||
|
||||
get().setStore(storeId, { provider });
|
||||
|
||||
return provider;
|
||||
},
|
||||
setStore: (storeId, props) => {
|
||||
set(({ docsStore }, ...store) => {
|
||||
return {
|
||||
...store,
|
||||
docsStore: {
|
||||
...docsStore,
|
||||
[storeId]: {
|
||||
...docsStore[storeId],
|
||||
...props,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
setCurrentDoc: (doc) => {
|
||||
set({ currentDoc: doc });
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,14 @@
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface UseEditorstore {
|
||||
editor?: BlockNoteEditor;
|
||||
setEditor: (editor: BlockNoteEditor | undefined) => void;
|
||||
}
|
||||
|
||||
export const useEditorStore = create<UseEditorstore>((set) => ({
|
||||
editor: undefined,
|
||||
setEditor: (editor) => {
|
||||
set({ editor });
|
||||
},
|
||||
}));
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export const randomColor = () => {
|
||||
const randomInt = (min: number, max: number) => {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
@@ -25,23 +23,5 @@ function hslToHex(h: number, s: number, l: number) {
|
||||
return `#${f(0)}${f(8)}${f(4)}`;
|
||||
}
|
||||
|
||||
export const toBase64 = (
|
||||
str: WithImplicitCoercion<ArrayBuffer | SharedArrayBuffer>,
|
||||
) => Buffer.from(str).toString('base64');
|
||||
|
||||
type BasicBlock = {
|
||||
type: string;
|
||||
content: string;
|
||||
};
|
||||
export const blocksToYDoc = (blocks: BasicBlock[], doc: Y.Doc) => {
|
||||
const xmlFragment = doc.getXmlFragment('document-store');
|
||||
|
||||
blocks.forEach((block) => {
|
||||
const xmlElement = new Y.XmlElement(block.type);
|
||||
if (block.content) {
|
||||
xmlElement.insert(0, [new Y.XmlText(block.content)]);
|
||||
}
|
||||
|
||||
xmlFragment.push([xmlElement]);
|
||||
});
|
||||
};
|
||||
export const toBase64 = (str: Uint8Array) =>
|
||||
Buffer.from(str).toString('base64');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Card, StyledLink, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
@@ -46,7 +47,11 @@ export const DocHeader = ({ doc, versionId }: DocHeaderProps) => {
|
||||
$theme="primary"
|
||||
$variation="600"
|
||||
$size="2rem"
|
||||
$css={`&:hover {background-color: ${colorsTokens()['primary-100']}; };`}
|
||||
$css={css`
|
||||
&:hover {
|
||||
background-color: ${colorsTokens()['primary-100']};
|
||||
}
|
||||
`}
|
||||
$hasTransition
|
||||
$radius="5px"
|
||||
$padding="tiny"
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
useTrans,
|
||||
useUpdateDoc,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
import { useBroadcastStore, useResponsiveStore } from '@/stores';
|
||||
import { isFirefox } from '@/utils/userAgent';
|
||||
|
||||
interface DocTitleProps {
|
||||
@@ -54,13 +54,17 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
const headingText = headings?.[0]?.contentText;
|
||||
const debounceRef = useRef<NodeJS.Timeout>();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
const { broadcast } = useBroadcastStore();
|
||||
|
||||
const { mutate: updateDoc } = useUpdateDoc({
|
||||
listInvalideQueries: [KEY_DOC, KEY_LIST_DOC],
|
||||
listInvalideQueries: [KEY_LIST_DOC],
|
||||
onSuccess(data) {
|
||||
if (data.title !== untitledDocument) {
|
||||
toast(t('Document title updated successfully'), VariantType.SUCCESS);
|
||||
}
|
||||
|
||||
// Broadcast to every user connected to the document
|
||||
broadcast(`${KEY_DOC}-${data.id}`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -100,6 +104,10 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTitleDisplay(doc.title);
|
||||
}, [doc.title]);
|
||||
|
||||
useEffect(() => {
|
||||
if ((!debounceRef.current && !isUntitled) || !headingText) {
|
||||
return;
|
||||
@@ -125,6 +133,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
$radius="4px"
|
||||
$padding={{ horizontal: 'tiny', vertical: '4px' }}
|
||||
$margin="none"
|
||||
$minWidth="200px"
|
||||
contentEditable={isFirefox() ? 'true' : 'plaintext-only'}
|
||||
onClick={handleOnClick}
|
||||
onBlurCapture={(e) =>
|
||||
|
||||
@@ -8,16 +8,18 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, DropButton, IconOptions } from '@/components';
|
||||
import { useAuthStore } from '@/core';
|
||||
import { useDocStore, usePanelEditorStore } from '@/features/docs/doc-editor/';
|
||||
import {
|
||||
useEditorStore,
|
||||
usePanelEditorStore,
|
||||
} from '@/features/docs/doc-editor/';
|
||||
import {
|
||||
Doc,
|
||||
ModalRemoveDoc,
|
||||
ModalShare,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { ModalVersion, Versions } from '@/features/docs/doc-versioning';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { ModalVersion, Versions } from '../../doc-versioning';
|
||||
|
||||
import { ModalPDF } from './ModalExport';
|
||||
|
||||
interface DocToolBoxProps {
|
||||
@@ -35,13 +37,12 @@ export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => {
|
||||
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
|
||||
const { isSmallMobile } = useResponsiveStore();
|
||||
const { authenticated } = useAuthStore();
|
||||
const { docsStore } = useDocStore();
|
||||
const { editor } = useEditorStore();
|
||||
const { toast } = useToastProvider();
|
||||
|
||||
const copyCurrentEditorToClipboard = async (
|
||||
asFormat: 'html' | 'markdown',
|
||||
) => {
|
||||
const editor = docsStore[doc.id]?.editor;
|
||||
if (!editor) {
|
||||
toast(t('Editor unavailable'), VariantType.ERROR, { duration: 3000 });
|
||||
return;
|
||||
|
||||
@@ -10,11 +10,11 @@ import {
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { t } from 'i18next';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
import { useDocStore } from '@/features/docs/doc-editor/';
|
||||
import { useEditorStore } from '@/features/docs/doc-editor';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
|
||||
import { useExport } from '../api/useExport';
|
||||
@@ -27,11 +27,12 @@ interface ModalPDFProps {
|
||||
}
|
||||
|
||||
export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { data: templates } = useTemplates({
|
||||
ordering: TemplatesOrdering.BY_CREATED_ON_DESC,
|
||||
});
|
||||
const { toast } = useToastProvider();
|
||||
const { docsStore } = useDocStore();
|
||||
const { editor } = useEditorStore();
|
||||
const {
|
||||
mutate: createExport,
|
||||
data: documentGenerated,
|
||||
@@ -104,8 +105,6 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = docsStore[doc.id].editor;
|
||||
|
||||
if (!editor) {
|
||||
toast(t('No editor found'), VariantType.ERROR);
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { Doc } from '@/features/docs';
|
||||
import { Doc, KEY_DOC } from '@/features/docs/doc-management';
|
||||
import { useBroadcastStore } from '@/stores';
|
||||
|
||||
export type UpdateDocLinkParams = Pick<Doc, 'id'> &
|
||||
Partial<Pick<Doc, 'link_role' | 'link_reach'>>;
|
||||
@@ -37,14 +38,20 @@ export function useUpdateDocLink({
|
||||
listInvalideQueries,
|
||||
}: UpdateDocLinkProps = {}) {
|
||||
const queryClient = useQueryClient();
|
||||
const { broadcast } = useBroadcastStore();
|
||||
|
||||
return useMutation<Doc, APIError, UpdateDocLinkParams>({
|
||||
mutationFn: updateDocLink,
|
||||
onSuccess: (data) => {
|
||||
onSuccess: (data, variable) => {
|
||||
listInvalideQueries?.forEach((queryKey) => {
|
||||
void queryClient.resetQueries({
|
||||
queryKey: [queryKey],
|
||||
});
|
||||
});
|
||||
|
||||
// Broadcast to every user connected to the document
|
||||
broadcast(`${KEY_DOC}-${variable.id}`);
|
||||
|
||||
onSuccess?.(data);
|
||||
},
|
||||
});
|
||||
|
||||
+6
-5
@@ -6,11 +6,11 @@ import {
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { t } from 'i18next';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Text, TextErrors } from '@/components';
|
||||
import useCunninghamTheme from '@/cunningham/useCunninghamTheme';
|
||||
import { useCunninghamTheme } from '@/cunningham/';
|
||||
|
||||
import { useRemoveDoc } from '../api/useRemoveDoc';
|
||||
import IconDoc from '../assets/icon-doc.svg';
|
||||
@@ -22,9 +22,10 @@ interface ModalRemoveDocProps {
|
||||
}
|
||||
|
||||
export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { toast } = useToastProvider();
|
||||
const router = useRouter();
|
||||
const { push } = useRouter();
|
||||
|
||||
const {
|
||||
mutate: removeDoc,
|
||||
@@ -35,7 +36,7 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
|
||||
toast(t('The document has been deleted.'), VariantType.SUCCESS, {
|
||||
duration: 4000,
|
||||
});
|
||||
router.push('/');
|
||||
void push('/');
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+2
-1
@@ -3,7 +3,7 @@ import {
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { t } from 'i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
import { Box, Card, IconBG, SideModal, Text } from '@/components';
|
||||
@@ -44,6 +44,7 @@ interface ModalShareProps {
|
||||
}
|
||||
|
||||
export const ModalShare = ({ onClose, doc }: ModalShareProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile, isSmallMobile } = useResponsiveStore();
|
||||
const width = isSmallMobile ? '100vw' : isMobile ? '90vw' : '70vw';
|
||||
const { toast } = useToastProvider();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './api';
|
||||
export * from './components';
|
||||
export * from './hooks';
|
||||
export * from './stores';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './useDocStore';
|
||||
@@ -0,0 +1,63 @@
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import * as Y from 'yjs';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { Base64, Doc, blocksToYDoc } from '@/features/docs/doc-management';
|
||||
|
||||
export interface UseDocStore {
|
||||
currentDoc?: Doc;
|
||||
providers: {
|
||||
[storeId: string]: HocuspocusProvider;
|
||||
};
|
||||
createProvider: (
|
||||
providerUrl: string,
|
||||
storeId: string,
|
||||
initialDoc: Base64,
|
||||
) => HocuspocusProvider;
|
||||
setProviders: (storeId: string, providers: HocuspocusProvider) => void;
|
||||
setCurrentDoc: (doc: Doc | undefined) => void;
|
||||
}
|
||||
|
||||
export const useDocStore = create<UseDocStore>((set, get) => ({
|
||||
currentDoc: undefined,
|
||||
providers: {},
|
||||
createProvider: (providerUrl, storeId, initialDoc) => {
|
||||
const doc = new Y.Doc({
|
||||
guid: storeId,
|
||||
});
|
||||
|
||||
if (initialDoc) {
|
||||
Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64'));
|
||||
} else {
|
||||
const initialDocContent = [
|
||||
{
|
||||
type: 'heading',
|
||||
content: '',
|
||||
},
|
||||
];
|
||||
|
||||
blocksToYDoc(initialDocContent, doc);
|
||||
}
|
||||
|
||||
const provider = new HocuspocusProvider({
|
||||
url: providerUrl,
|
||||
name: storeId,
|
||||
document: doc,
|
||||
});
|
||||
|
||||
get().setProviders(storeId, provider);
|
||||
|
||||
return provider;
|
||||
},
|
||||
setProviders: (storeId, provider) => {
|
||||
set(({ providers }) => ({
|
||||
providers: {
|
||||
...providers,
|
||||
[storeId]: provider,
|
||||
},
|
||||
}));
|
||||
},
|
||||
setCurrentDoc: (doc) => {
|
||||
set({ currentDoc: doc });
|
||||
},
|
||||
}));
|
||||
@@ -1,3 +1,5 @@
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { Doc, Role } from './types';
|
||||
|
||||
export const currentDocRole = (abilities: Doc['abilities']): Role => {
|
||||
@@ -9,3 +11,20 @@ export const currentDocRole = (abilities: Doc['abilities']): Role => {
|
||||
? Role.EDITOR
|
||||
: Role.READER;
|
||||
};
|
||||
|
||||
type BasicBlock = {
|
||||
type: string;
|
||||
content: string;
|
||||
};
|
||||
export const blocksToYDoc = (blocks: BasicBlock[], doc: Y.Doc) => {
|
||||
const xmlFragment = doc.getXmlFragment('document-store');
|
||||
|
||||
blocks.forEach((block) => {
|
||||
const xmlElement = new Y.XmlElement(block.type);
|
||||
if (block.content) {
|
||||
xmlElement.insert(0, [new Y.XmlText(block.content)]);
|
||||
}
|
||||
|
||||
xmlFragment.push([xmlElement]);
|
||||
});
|
||||
};
|
||||
|
||||
+3
-6
@@ -2,22 +2,19 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, BoxButton, Text } from '@/components';
|
||||
import { HeadingBlock, useDocStore } from '@/features/docs/doc-editor';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import { HeadingBlock, useEditorStore } from '@/features/docs/doc-editor';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { Heading } from './Heading';
|
||||
|
||||
interface TableContentProps {
|
||||
doc: Doc;
|
||||
headings: HeadingBlock[];
|
||||
}
|
||||
|
||||
export const TableContent = ({ doc, headings }: TableContentProps) => {
|
||||
const { docsStore } = useDocStore();
|
||||
export const TableContent = ({ headings }: TableContentProps) => {
|
||||
const { editor } = useEditorStore();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
const { t } = useTranslation();
|
||||
const editor = docsStore?.[doc.id]?.editor;
|
||||
const [headingIdHighlight, setHeadingIdHighlight] = useState<string>();
|
||||
|
||||
// To highlight the first heading in the viewport
|
||||
|
||||
+13
-16
@@ -6,13 +6,13 @@ import {
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { t } from 'i18next';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
import { toBase64, useDocStore } from '@/features/docs/doc-editor';
|
||||
import { Doc, useUpdateDoc } from '@/features/docs/doc-management';
|
||||
import { toBase64 } from '@/features/docs/doc-editor';
|
||||
import { Doc, useDocStore, useUpdateDoc } from '@/features/docs/doc-management';
|
||||
|
||||
import { KEY_LIST_DOC_VERSIONS } from '../api/useDocVersions';
|
||||
import { Versions } from '../types';
|
||||
@@ -30,30 +30,27 @@ export const ModalVersion = ({
|
||||
docId,
|
||||
versionId,
|
||||
}: ModalVersionProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToastProvider();
|
||||
const router = useRouter();
|
||||
const { docsStore, setStore } = useDocStore();
|
||||
const { push } = useRouter();
|
||||
const { providers } = useDocStore();
|
||||
const { mutate: updateDoc } = useUpdateDoc({
|
||||
listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
|
||||
onSuccess: () => {
|
||||
const onDisplaySuccess = () => {
|
||||
toast(t('Version restored successfully'), VariantType.SUCCESS);
|
||||
router.push(`/docs/${docId}`);
|
||||
void push(`/docs/${docId}`);
|
||||
};
|
||||
|
||||
if (!docsStore?.[docId]?.provider || !docsStore?.[versionId]?.provider) {
|
||||
if (!providers?.[docId] || !providers?.[versionId]) {
|
||||
onDisplaySuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
setStore(docId, {
|
||||
editor: undefined,
|
||||
});
|
||||
|
||||
revertUpdate(
|
||||
docsStore[docId].provider.document,
|
||||
docsStore[docId].provider.document,
|
||||
docsStore[versionId].provider.document,
|
||||
providers[docId].document,
|
||||
providers[docId].document,
|
||||
providers[versionId].document,
|
||||
);
|
||||
|
||||
onDisplaySuccess();
|
||||
@@ -83,7 +80,7 @@ export const ModalVersion = ({
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
const newDoc = toBase64(
|
||||
Y.encodeStateAsUpdate(docsStore?.[versionId]?.provider.document),
|
||||
Y.encodeStateAsUpdate(providers?.[versionId].document),
|
||||
);
|
||||
|
||||
updateDoc({
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { t } from 'i18next';
|
||||
import React, { PropsWithChildren, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, DropButton, IconOptions, StyledLink, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
@@ -25,6 +25,7 @@ export const VersionItem = ({
|
||||
link,
|
||||
isActive,
|
||||
}: VersionItemProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const [isDropOpen, setIsDropOpen] = useState(false);
|
||||
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -12,12 +12,12 @@ import { DocsGrid } from './DocsGrid';
|
||||
export const DocsGridContainer = () => {
|
||||
const { t } = useTranslation();
|
||||
const { untitledDocument } = useTrans();
|
||||
const router = useRouter();
|
||||
const { push } = useRouter();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
|
||||
const { mutate: createDoc } = useCreateDoc({
|
||||
onSuccess: (doc) => {
|
||||
router.push(`/docs/${doc.id}`);
|
||||
void push(`/docs/${doc.id}`);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+8
-1
@@ -5,11 +5,13 @@ import { User } from '@/core/auth';
|
||||
import {
|
||||
Access,
|
||||
Doc,
|
||||
KEY_DOC,
|
||||
KEY_LIST_DOC,
|
||||
Role,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { KEY_LIST_DOC_ACCESSES } from '@/features/docs/members/members-list';
|
||||
import { ContentLanguage } from '@/i18n/types';
|
||||
import { useBroadcastStore } from '@/stores';
|
||||
|
||||
import { OptionType } from '../types';
|
||||
|
||||
@@ -53,9 +55,11 @@ export const createDocAccess = async ({
|
||||
|
||||
export function useCreateDocAccess() {
|
||||
const queryClient = useQueryClient();
|
||||
const { broadcast } = useBroadcastStore();
|
||||
|
||||
return useMutation<Access, APIError, CreateDocAccessParams>({
|
||||
mutationFn: createDocAccess,
|
||||
onSuccess: () => {
|
||||
onSuccess: (_data, variable) => {
|
||||
void queryClient.resetQueries({
|
||||
queryKey: [KEY_LIST_DOC],
|
||||
});
|
||||
@@ -65,6 +69,9 @@ export function useCreateDocAccess() {
|
||||
void queryClient.resetQueries({
|
||||
queryKey: [KEY_LIST_DOC_ACCESSES],
|
||||
});
|
||||
|
||||
// Broadcast to every user connected to the document
|
||||
broadcast(`${KEY_DOC}-${variable.docId}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+7
@@ -7,6 +7,7 @@ import {
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { KEY_DOC, KEY_LIST_DOC } from '@/features/docs/doc-management';
|
||||
import { KEY_LIST_USER } from '@/features/docs/members/members-add';
|
||||
import { useBroadcastStore } from '@/stores';
|
||||
|
||||
import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses';
|
||||
|
||||
@@ -39,6 +40,8 @@ type UseDeleteDocAccessOptions = UseMutationOptions<
|
||||
|
||||
export const useDeleteDocAccess = (options?: UseDeleteDocAccessOptions) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { broadcast } = useBroadcastStore();
|
||||
|
||||
return useMutation<void, APIError, DeleteDocAccessProps>({
|
||||
mutationFn: deleteDocAccess,
|
||||
...options,
|
||||
@@ -49,6 +52,10 @@ export const useDeleteDocAccess = (options?: UseDeleteDocAccessOptions) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_DOC],
|
||||
});
|
||||
|
||||
// Broadcast to every user connected to the document
|
||||
broadcast(`${KEY_DOC}-${variables.docId}`);
|
||||
|
||||
void queryClient.resetQueries({
|
||||
queryKey: [KEY_LIST_DOC],
|
||||
});
|
||||
|
||||
+7
@@ -11,6 +11,7 @@ import {
|
||||
KEY_LIST_DOC,
|
||||
Role,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { useBroadcastStore } from '@/stores';
|
||||
|
||||
import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses';
|
||||
|
||||
@@ -49,6 +50,8 @@ type UseUpdateDocAccessOptions = UseMutationOptions<
|
||||
|
||||
export const useUpdateDocAccess = (options?: UseUpdateDocAccessOptions) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { broadcast } = useBroadcastStore();
|
||||
|
||||
return useMutation<Access, APIError, UpdateDocAccessProps>({
|
||||
mutationFn: updateDocAccess,
|
||||
...options,
|
||||
@@ -59,6 +62,10 @@ export const useUpdateDocAccess = (options?: UseUpdateDocAccessOptions) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_DOC],
|
||||
});
|
||||
|
||||
// Broadcast to every user connected to the document
|
||||
broadcast(`${KEY_DOC}-${variables.docId}`);
|
||||
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_LIST_DOC],
|
||||
});
|
||||
|
||||
+3
-3
@@ -5,7 +5,7 @@ import {
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -35,7 +35,7 @@ export const MemberItem = ({
|
||||
const { isSmallMobile, screenWidth } = useResponsiveStore();
|
||||
const [localRole, setLocalRole] = useState(role);
|
||||
const { toast } = useToastProvider();
|
||||
const router = useRouter();
|
||||
const { push } = useRouter();
|
||||
const { mutate: updateDocAccess, error: errorUpdate } = useUpdateDocAccess({
|
||||
onSuccess: () => {
|
||||
toast(t('The role has been updated'), VariantType.SUCCESS, {
|
||||
@@ -55,7 +55,7 @@ export const MemberItem = ({
|
||||
);
|
||||
|
||||
if (isMyself) {
|
||||
router.push('/');
|
||||
void push('/');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Image from 'next/image';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ export type RequestData = {
|
||||
url: string;
|
||||
method?: string;
|
||||
headers: Record<string, string>;
|
||||
body?: ArrayBuffer;
|
||||
body?: ArrayBufferLike;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[key: string]: any;
|
||||
};
|
||||
@@ -48,12 +48,12 @@ export class RequestSerializer {
|
||||
return new RequestSerializer(requestData);
|
||||
}
|
||||
|
||||
public static arrayBufferToString(buffer: ArrayBuffer) {
|
||||
public static arrayBufferToString(buffer: ArrayBufferLike) {
|
||||
const decoder = new TextDecoder();
|
||||
return decoder.decode(buffer);
|
||||
return decoder.decode(buffer as ArrayBuffer);
|
||||
}
|
||||
|
||||
public static arrayBufferToJson<T>(buffer: ArrayBuffer) {
|
||||
public static arrayBufferToJson<T>(buffer: ArrayBufferLike) {
|
||||
const jsonString = RequestSerializer.arrayBufferToString(buffer);
|
||||
return JSON.parse(jsonString) as T;
|
||||
}
|
||||
@@ -64,7 +64,9 @@ export class RequestSerializer {
|
||||
}
|
||||
|
||||
public static objectToArrayBuffer(ob: Record<string, unknown>) {
|
||||
return RequestSerializer.stringToArrayBuffer(JSON.stringify(ob));
|
||||
return RequestSerializer.stringToArrayBuffer(
|
||||
JSON.stringify(ob),
|
||||
) as ArrayBuffer;
|
||||
}
|
||||
|
||||
constructor(requestData: RequestData) {
|
||||
@@ -85,7 +87,7 @@ export class RequestSerializer {
|
||||
|
||||
toRequest(): Request {
|
||||
const { url, ...rest } = this._requestData;
|
||||
return new Request(url, rest);
|
||||
return new Request(url, { ...rest, body: rest.body as BodyInit });
|
||||
}
|
||||
|
||||
clone(): RequestSerializer {
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import Head from 'next/head';
|
||||
import { useRouter as useNavigate } from 'next/navigation';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
import { TextErrors } from '@/components/TextErrors';
|
||||
import { useCollaborationUrl } from '@/core';
|
||||
import { useAuthStore } from '@/core/auth';
|
||||
import { DocEditor, useDocStore } from '@/features/docs';
|
||||
import { useDoc } from '@/features/docs/doc-management';
|
||||
import { DocEditor } from '@/features/docs/doc-editor';
|
||||
import { KEY_DOC, useDoc, useDocStore } from '@/features/docs/doc-management';
|
||||
import { MainLayout } from '@/layouts';
|
||||
import { useBroadcastStore } from '@/stores';
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
export function DocLayout() {
|
||||
@@ -41,9 +43,12 @@ const DocPage = ({ id }: DocProps) => {
|
||||
const { login } = useAuthStore();
|
||||
const { data: docQuery, isError, error } = useDoc({ id });
|
||||
const [doc, setDoc] = useState(docQuery);
|
||||
const { setCurrentDoc } = useDocStore();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { setCurrentDoc, createProvider, providers } = useDocStore();
|
||||
const { setBroadcastProvider, addTask } = useBroadcastStore();
|
||||
const queryClient = useQueryClient();
|
||||
const { replace } = useRouter();
|
||||
const provider = providers?.[id];
|
||||
const collaborationUrl = useCollaborationUrl(doc?.id);
|
||||
|
||||
useEffect(() => {
|
||||
if (doc?.title) {
|
||||
@@ -66,9 +71,38 @@ const DocPage = ({ id }: DocProps) => {
|
||||
};
|
||||
}, [docQuery, setCurrentDoc]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!doc?.id || !collaborationUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newProvider = provider;
|
||||
if (!provider || provider.document.guid !== doc.id) {
|
||||
newProvider = createProvider(collaborationUrl, doc.id, doc.content);
|
||||
}
|
||||
|
||||
setBroadcastProvider(newProvider);
|
||||
}, [createProvider, doc, provider, setBroadcastProvider, collaborationUrl]);
|
||||
|
||||
/**
|
||||
* We add a broadcast task to reset the query cache
|
||||
* when the document visibility changes.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!doc?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
addTask(`${KEY_DOC}-${doc.id}`, () => {
|
||||
void queryClient.resetQueries({
|
||||
queryKey: [KEY_DOC, { id: doc.id }],
|
||||
});
|
||||
});
|
||||
}, [addTask, doc?.id, queryClient]);
|
||||
|
||||
if (isError && error) {
|
||||
if (error.status === 404) {
|
||||
navigate.replace(`/404`);
|
||||
void replace(`/404`);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Configure Crisp chat for real-time support across all pages.
|
||||
*/
|
||||
|
||||
import { Crisp } from 'crisp-sdk-web';
|
||||
|
||||
import { User } from '@/core';
|
||||
|
||||
export const initializeCrispSession = (user: User) => {
|
||||
if (!Crisp.isCrispInjected()) {
|
||||
return;
|
||||
}
|
||||
Crisp.setTokenId(`impress-${user.id}`);
|
||||
Crisp.user.setEmail(user.email);
|
||||
};
|
||||
|
||||
export const configureCrispSession = (websiteId: string) => {
|
||||
if (Crisp.isCrispInjected()) {
|
||||
return;
|
||||
}
|
||||
Crisp.configure(websiteId);
|
||||
Crisp.setSafeMode(true);
|
||||
};
|
||||
|
||||
export const terminateCrispSession = () => {
|
||||
if (!Crisp.isCrispInjected()) {
|
||||
return;
|
||||
}
|
||||
Crisp.setTokenId();
|
||||
Crisp.session.reset();
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Crisp';
|
||||
@@ -1 +1,2 @@
|
||||
export * from './useBroadcastStore';
|
||||
export * from './useResponsiveStore';
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import * as Y from 'yjs';
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface BroadcastState {
|
||||
addTask: (taskLabel: string, action: () => void) => void;
|
||||
broadcast: (taskLabel: string) => void;
|
||||
getBroadcastProvider: () => HocuspocusProvider | undefined;
|
||||
provider?: HocuspocusProvider;
|
||||
setBroadcastProvider: (provider: HocuspocusProvider) => void;
|
||||
tasks: { [taskLabel: string]: Y.Array<string> };
|
||||
}
|
||||
|
||||
export const useBroadcastStore = create<BroadcastState>((set, get) => ({
|
||||
provider: undefined,
|
||||
tasks: {},
|
||||
setBroadcastProvider: (provider) => set({ provider }),
|
||||
getBroadcastProvider: () => {
|
||||
const provider = get().provider;
|
||||
if (!provider) {
|
||||
console.warn('Provider is not defined');
|
||||
return;
|
||||
}
|
||||
|
||||
return provider;
|
||||
},
|
||||
addTask: (taskLabel, action) => {
|
||||
const taskExistAlready = get().tasks[taskLabel];
|
||||
const provider = get().getBroadcastProvider();
|
||||
if (taskExistAlready || !provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
const task = provider.document.getArray<string>(taskLabel);
|
||||
task.observe(() => {
|
||||
action();
|
||||
});
|
||||
|
||||
set((state) => ({
|
||||
tasks: {
|
||||
...state.tasks,
|
||||
[taskLabel]: task,
|
||||
},
|
||||
}));
|
||||
},
|
||||
broadcast: (taskLabel) => {
|
||||
const task = get().tasks[taskLabel];
|
||||
if (!task) {
|
||||
console.warn(`Task ${taskLabel} is not defined`);
|
||||
return;
|
||||
}
|
||||
task.push([`broadcast: ${taskLabel}`]);
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import type { Client } from '@sentry/types';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import packageJson from '../../package.json';
|
||||
|
||||
interface SentryState {
|
||||
sentry?: Client;
|
||||
setSentry: (dsn?: string, environment?: string) => void;
|
||||
}
|
||||
|
||||
export const useSentryStore = create<SentryState>((set, get) => ({
|
||||
sentry: undefined,
|
||||
setSentry: (dsn, environment) => {
|
||||
const sentry = get().sentry;
|
||||
if (sentry) {
|
||||
return;
|
||||
}
|
||||
|
||||
set({
|
||||
sentry: Sentry.init({
|
||||
dsn,
|
||||
environment,
|
||||
integrations: [Sentry.replayIntegration()],
|
||||
release: packageJson.version,
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
tracesSampleRate: 1.0,
|
||||
}),
|
||||
});
|
||||
},
|
||||
}));
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impress",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.1",
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
@@ -25,18 +25,18 @@
|
||||
"i18n:test": "yarn I18N run test"
|
||||
},
|
||||
"resolutions": {
|
||||
"@blocknote/core": "0.17.1",
|
||||
"@blocknote/mantine": "0.17.1",
|
||||
"@blocknote/react": "0.17.1",
|
||||
"@types/node": "20.16.13",
|
||||
"@blocknote/core": "0.19.2",
|
||||
"@blocknote/mantine": "0.19.2",
|
||||
"@blocknote/react": "0.19.2",
|
||||
"@types/node": "22.9.3",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.10.0",
|
||||
"@typescript-eslint/parser": "8.10.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.15.0",
|
||||
"@typescript-eslint/parser": "8.15.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.57.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"typescript": "5.6.3",
|
||||
"typescript": "5.7.2",
|
||||
"yjs": "13.6.20"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
{
|
||||
"name": "eslint-config-impress",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.1",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "14.2.15",
|
||||
"@tanstack/eslint-plugin-query": "5.59.7",
|
||||
"@next/eslint-plugin-next": "15.0.3",
|
||||
"@tanstack/eslint-plugin-query": "5.61.3",
|
||||
"@typescript-eslint/eslint-plugin": "*",
|
||||
"@typescript-eslint/parser": "*",
|
||||
"eslint": "*",
|
||||
"eslint-config-next": "14.2.15",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-jest": "28.8.3",
|
||||
"eslint-plugin-jsx-a11y": "6.10.1",
|
||||
"eslint-plugin-playwright": "1.8.0",
|
||||
"eslint-plugin-jest": "28.9.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-playwright": "2.1.0",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"eslint-plugin-react": "7.37.1",
|
||||
"eslint-plugin-testing-library": "6.4.0",
|
||||
"eslint-plugin-react": "7.37.2",
|
||||
"eslint-plugin-testing-library": "7.0.0",
|
||||
"prettier": "3.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "packages-i18n",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"extract-translation": "yarn extract-translation:impress",
|
||||
@@ -12,7 +12,7 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/jest": "29.5.13",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "*",
|
||||
"eslint-config-impress": "*",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server-y-provider",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.1",
|
||||
"description": "Y.js provider for docs",
|
||||
"repository": "https://github.com/numerique-gouv/impress",
|
||||
"license": "MIT",
|
||||
@@ -15,7 +15,7 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hocuspocus/server": "2.13.7",
|
||||
"@hocuspocus/server": "2.14.0",
|
||||
"y-protocols": "1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+3787
-2880
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
djangoSuperUserEmail: ENC[AES256_GCM,data:7b1xfYmr1g0RlBmsHBRA39ZPV/6+1DrtHQ==,iv:/GW7oLxPTZYmRWVPvyAQMoZl1owHM4Fo0XAOtyEh2rA=,tag:DaqoW+dglyAOXMm5+mrDfA==,type:str]
|
||||
djangoSuperUserPass: ENC[AES256_GCM,data:RQgX,iv:q3CdfmwGfHSTjLXTimDk/1MyoFLviRuwmZa2E7GUzhY=,tag:HCtdtqgSxdJIHFhI8xpegQ==,type:str]
|
||||
djangoSecretKey: ENC[AES256_GCM,data:9fr7VwwXN6+9+rdDtgeDuEbq6R2Gb0JhifUgxTPVbd4usFQv1AUVkxF40fu5nYBmM8vk,iv:X44837MB7NQZ1J0o0JPDK+2g5eqbCzo9mDPJTz/bKSk=,tag:Ju4l5Pi8ccNASdiwFVFKgg==,type:str]
|
||||
oidc:
|
||||
clientId: ENC[AES256_GCM,data:wndPCbysbWDybdHglcG+wkMWk1rrD40hKqFxct9T3TLEGOk/,iv:RH1OdBX1GYIT90sSq0AGz49fFi6dL0m49Pegs6Ko9tQ=,tag:/tKytQwoZkBX1Tf96gAjIA==,type:str]
|
||||
clientSecret: ENC[AES256_GCM,data:MUJ0wsg+LC2QZ1jZ0Twd3FS3dQevmJq9/97qVI3ARHuJIVlQz0Qah4vE7/iR+sn7ME2o1s1AzV4c1Yx/F3nHBg==,iv:LvinICSzF/8EvrHZD4Jp6lt7g3yxSOEgVHPrc3SShjo=,tag:yvkyyBXmhEkmGL7jZevUCA==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- recipient: age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMMjFCeWhkUmRWTnlIM1JM
|
||||
dVFock1DWmtXQnpQZWZMWW1YdndhSS93MlVFCmxKVDUwOUt0NjJIZiswSm5aRi9U
|
||||
VEllelBZVmFKdVFzcVJPUm50VHo5RTgKLS0tIDlkU3htTEdSREFOSUxlTGVtUm1n
|
||||
RzJZbzhFcDNZKzdxMWFHTWx6Uy9GVFkKTw8LbhzAACp0NUHDfNcXpZyr2pJyNxxw
|
||||
C7j/UB0cAejlSJHaUUiZ6TEcslXRpqnNagwUw4z/uzo7m4temay22A==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQQjBNMnVlNURQVWdjSyty
|
||||
RGozcmN5eTUwRHJIWnhhc1E3U1NXQ3AwTWxBCnFjbmJNZnFiRVJ6VHhmQmt1Vk5n
|
||||
OTVXWVh3RzhoMWNrbUl6OHphTjFLQVUKLS0tIGJjUlNhK0dHQ2R3SCtrbTRnaFJT
|
||||
Q1pyRXhSVm8xQWk2NG1MK0srVU1pL2sKkoxGCM00UM2leTNCn5H8499uwJw1NIXs
|
||||
PoRNgplehrHFptrAwGEpSYMXbxu88N7EWa/rtOp+sHWK5zpxscMkjA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzYnpkYnJnYnJjVFRHRzRa
|
||||
N09JOXVnQkVrcVcwdk9kR1k1azNib2lkMVZFCmhvOHlpVnJ0RlRpYWZ1TkVoaklV
|
||||
NmNzY3BEeWN1MUtKWmZFT2RaMUxBRW8KLS0tIG92ZmhsZ29LSkRSREhiaG9kWXhH
|
||||
akREb0ttYVpNWTJHb1pjaWRFbWpxUjgKgZp3cN2rZw4ktbpb5cUnDEtsT/KWszGi
|
||||
pmpJHgsMADigyUc+Pjw+1pwpn0FtXVEXGedbf8bBuJavvbS2PuJBsg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxaHZJeStiVnBzTGNTNzdo
|
||||
UDFVTU51ZWp0WWorUnBlSzVBSU9IU2JnbUNNCkpMZGdNV3FUYkZOcWNLK0JWci81
|
||||
WGNwYi9Jb0QrV0lkUzNJWTcrUjIzUmMKLS0tIHlTKzNsVzNsSGFuYjJ0RFp0Y1Nr
|
||||
a1VOcDBPTTYvNjkxN092N1UrYk1CM2cKNifC3ZLOrFTFKA9iKg8nPpZb+3DxnTwq
|
||||
grsrxQa40b/Vv/aPoiPBMeSENDcH48X/EhMFNKX7dvl+7HEaY+QPlA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1hnhuzj96ktkhpyygvmz0x9h8mfvssz7ss6emmukags644mdhf4msajk93r
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoZ2ZlcllJeGlKUDNxUk1w
|
||||
ekZ3TSttaXREV1FBRWwzNW54cjlYbHpLdWpRCnhSL2hEVVBEWEJKQWF0YTk1YzhJ
|
||||
RTBGN25sT0hBM3V4QndiTVkveDBwQ2cKLS0tIEdoZGRLRXdCME1wcUJHQXhtSHBQ
|
||||
UVEyNUVIanF6Z3ZSUjU1aTk0NFRBR0EKGuH5vzOV9lP/qRew0maECapKtLILaf/4
|
||||
XoSgPnjh8pIbJG7i9VKnFORlzkNJ6OPhZlX3ax15hd1qQv0PSCMBDA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2024-11-02T06:36:16Z"
|
||||
mac: ENC[AES256_GCM,data:CFU67noumihiYd0zSQex6Bgs5e/w3v3a9Ywd2XX53mx6W16w8DGyMykjaBzwX+wKC9oTqEmBXmmixf8NpQRuG9owcf9GIsFy1cK+69y+ISQINxBqxMvYouaC7UQeywpC1b9gHw7sVU1GCAiY6Ha+lPHvEavelbGWn/MSVyaBB2k=,iv:m1ShIjNGFjcC0N5mjvhbgxnVN7PcpSkBxMquUlsROCk=,tag:XTNxFRMQslbpvbL9gzMxHA==,type:str]
|
||||
pgp: []
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.9.0
|
||||
@@ -0,0 +1,123 @@
|
||||
image:
|
||||
repository: localhost:5001/impress-backend
|
||||
pullPolicy: Always
|
||||
tag: "latest"
|
||||
|
||||
backend:
|
||||
replicas: 1
|
||||
envVars:
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS: https://impress.127.0.0.1.nip.io,http://impress.127.0.0.1.nip.io
|
||||
DJANGO_CONFIGURATION: Production
|
||||
DJANGO_ALLOWED_HOSTS: "*"
|
||||
DJANGO_SECRET_KEY: {{ .Values.djangoSecretKey }}
|
||||
DJANGO_SETTINGS_MODULE: impress.settings
|
||||
DJANGO_SUPERUSER_PASSWORD: admin
|
||||
DJANGO_EMAIL_HOST: "mailcatcher"
|
||||
DJANGO_EMAIL_PORT: 1025
|
||||
DJANGO_EMAIL_USE_SSL: False
|
||||
LOGGING_LEVEL_HANDLERS_CONSOLE: ERROR
|
||||
LOGGING_LEVEL_LOGGERS_ROOT: INFO
|
||||
LOGGING_LEVEL_LOGGERS_APP: INFO
|
||||
OIDC_OP_JWKS_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/jwks
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/authorize
|
||||
OIDC_OP_TOKEN_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/token
|
||||
OIDC_OP_USER_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/userinfo
|
||||
OIDC_OP_LOGOUT_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/session/end
|
||||
OIDC_RP_CLIENT_ID: {{ .Values.oidc.clientId }}
|
||||
OIDC_RP_CLIENT_SECRET: {{ .Values.oidc.clientSecret }}
|
||||
OIDC_RP_SIGN_ALGO: RS256
|
||||
OIDC_RP_SCOPES: "openid email"
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS: https://impress.127.0.0.1.nip.io
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
|
||||
LOGIN_REDIRECT_URL: https://impress.127.0.0.1.nip.io
|
||||
LOGIN_REDIRECT_URL_FAILURE: https://impress.127.0.0.1.nip.io
|
||||
LOGOUT_REDIRECT_URL: https://impress.127.0.0.1.nip.io
|
||||
DB_HOST: postgres-postgresql
|
||||
DB_NAME: impress
|
||||
DB_USER: dinum
|
||||
DB_PASSWORD: pass
|
||||
DB_PORT: 5432
|
||||
POSTGRES_DB: impress
|
||||
POSTGRES_USER: dinum
|
||||
POSTGRES_PASSWORD: pass
|
||||
REDIS_URL: redis://default:pass@redis-master:6379/1
|
||||
AWS_S3_ENDPOINT_URL: http://minio.impress.svc.cluster.local:9000
|
||||
AWS_S3_ACCESS_KEY_ID: impress
|
||||
AWS_S3_SECRET_ACCESS_KEY: password
|
||||
AWS_STORAGE_BUCKET_NAME: impress-media-storage
|
||||
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
||||
|
||||
migrate:
|
||||
command:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- |
|
||||
python manage.py migrate --no-input &&
|
||||
python manage.py create_demo --force
|
||||
restartPolicy: Never
|
||||
|
||||
command:
|
||||
- "gunicorn"
|
||||
- "-c"
|
||||
- "/usr/local/etc/gunicorn/impress.py"
|
||||
- "impress.wsgi:application"
|
||||
- "--reload"
|
||||
|
||||
createsuperuser:
|
||||
command:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- |
|
||||
python manage.py createsuperuser --email admin@example.com --password admin
|
||||
restartPolicy: Never
|
||||
|
||||
frontend:
|
||||
envVars:
|
||||
PORT: 8080
|
||||
NEXT_PUBLIC_API_ORIGIN: https://impress.127.0.0.1.nip.io
|
||||
NEXT_PUBLIC_Y_PROVIDER_URL: wss://impress.127.0.0.1.nip.io/ws
|
||||
NEXT_PUBLIC_MEDIA_URL: https://impress.127.0.0.1.nip.io
|
||||
|
||||
replicas: 1
|
||||
command:
|
||||
- yarn
|
||||
- dev
|
||||
|
||||
image:
|
||||
repository: localhost:5001/impress-frontend
|
||||
pullPolicy: Always
|
||||
tag: "latest"
|
||||
|
||||
yProvider:
|
||||
replicas: 1
|
||||
|
||||
image:
|
||||
repository: localhost:5001/impress-y-provider
|
||||
pullPolicy: Always
|
||||
tag: "latest"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
host: impress.127.0.0.1.nip.io
|
||||
|
||||
ingressWS:
|
||||
enabled: true
|
||||
host: impress.127.0.0.1.nip.io
|
||||
|
||||
ingressAdmin:
|
||||
enabled: true
|
||||
host: impress.127.0.0.1.nip.io
|
||||
|
||||
ingressMedia:
|
||||
enabled: true
|
||||
host: impress.127.0.0.1.nip.io
|
||||
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/auth-url: https://impress.127.0.0.1.nip.io/api/v1.0/documents/retrieve-auth/
|
||||
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
|
||||
nginx.ingress.kubernetes.io/upstream-vhost: minio.impress.svc.cluster.local:9000
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /impress-media-storage/$1
|
||||
|
||||
serviceMedia:
|
||||
host: minio.impress.svc.cluster.local
|
||||
port: 9000
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: v2
|
||||
name: extra
|
||||
description: A Helm chart to add some manifests to impress
|
||||
type: application
|
||||
version: 1.8.1
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: core.libre.sh/v1alpha1
|
||||
kind: Redis
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
disableAuth: false
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: core.libre.sh/v1alpha1
|
||||
kind: Postgres
|
||||
metadata:
|
||||
name: postgresql
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
database: impress
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: core.libre.sh/v1alpha1
|
||||
kind: Bucket
|
||||
metadata:
|
||||
name: impress-media-storage
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
provider: data
|
||||
versioned: true
|
||||
@@ -0,0 +1,67 @@
|
||||
repositories:
|
||||
- name: bitnami
|
||||
url: registry-1.docker.io/bitnamicharts
|
||||
oci: true
|
||||
|
||||
releases:
|
||||
- name: postgres
|
||||
installed: {{ eq .Environment.Name "dev" | toYaml }}
|
||||
namespace: {{ .Namespace }}
|
||||
chart: bitnami/postgresql
|
||||
version: 13.1.5
|
||||
values:
|
||||
- auth:
|
||||
username: dinum
|
||||
password: pass
|
||||
database: impress
|
||||
- tls:
|
||||
enabled: true
|
||||
autoGenerated: true
|
||||
|
||||
- name: minio
|
||||
installed: {{ eq .Environment.Name "dev" | toYaml }}
|
||||
namespace: {{ .Namespace }}
|
||||
chart: bitnami/minio
|
||||
version: 12.10.10
|
||||
values:
|
||||
- auth:
|
||||
rootUser: impress
|
||||
rootPassword: password
|
||||
- provisioning:
|
||||
enabled: true
|
||||
buckets:
|
||||
- name: impress-media-storage
|
||||
versioning: true
|
||||
|
||||
- name: redis
|
||||
installed: {{ eq .Environment.Name "dev" | toYaml }}
|
||||
namespace: {{ .Namespace }}
|
||||
chart: bitnami/redis
|
||||
version: 18.19.2
|
||||
values:
|
||||
- auth:
|
||||
password: pass
|
||||
architecture: standalone
|
||||
|
||||
- name: extra
|
||||
installed: {{ ne .Environment.Name "dev" | toYaml }}
|
||||
namespace: {{ .Namespace }}
|
||||
chart: ./extra
|
||||
secrets:
|
||||
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
||||
|
||||
- name: impress
|
||||
version: {{ .Values.version }}
|
||||
namespace: {{ .Namespace }}
|
||||
chart: ./impress
|
||||
values:
|
||||
- env.d/{{ .Environment.Name }}/values.impress.yaml.gotmpl
|
||||
secrets:
|
||||
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
||||
|
||||
environments:
|
||||
dev:
|
||||
values:
|
||||
- version: 1.8.1
|
||||
secrets:
|
||||
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user