Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ddac6197e3 |
@@ -34,4 +34,3 @@ db.sqlite3
|
||||
|
||||
# Frontend
|
||||
node_modules
|
||||
.next
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Set the default behavior for all files
|
||||
* text=auto eol=lf
|
||||
|
||||
# Binary files (should not be modified)
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.mov binary
|
||||
*.mp4 binary
|
||||
*.mp3 binary
|
||||
*.flv binary
|
||||
*.fla binary
|
||||
*.swf binary
|
||||
*.gz binary
|
||||
*.zip binary
|
||||
*.7z binary
|
||||
*.ttf binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.eot binary
|
||||
*.pdf binary
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
install-dependencies:
|
||||
uses: ./.github/workflows/dependencies.yml
|
||||
with:
|
||||
node_version: '22.x'
|
||||
node_version: '20.x'
|
||||
with-front-dependencies-installation: true
|
||||
|
||||
synchronize-with-crowdin:
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
install-dependencies:
|
||||
uses: ./.github/workflows/dependencies.yml
|
||||
with:
|
||||
node_version: '22.x'
|
||||
node_version: '20.x'
|
||||
with-front-dependencies-installation: true
|
||||
with-build_mails: true
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
inputs:
|
||||
node_version:
|
||||
required: false
|
||||
default: '22.x'
|
||||
default: '20.x'
|
||||
type: string
|
||||
with-front-dependencies-installation:
|
||||
type: boolean
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
install-dependencies:
|
||||
uses: ./.github/workflows/dependencies.yml
|
||||
with:
|
||||
node_version: '22.x'
|
||||
node_version: '20.x'
|
||||
with-front-dependencies-installation: true
|
||||
|
||||
test-front:
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22.x"
|
||||
node-version: "20.x"
|
||||
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v4
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22.x"
|
||||
node-version: "20.x"
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22.x"
|
||||
node-version: "20.x"
|
||||
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v4
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22.x"
|
||||
node-version: "20.x"
|
||||
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v4
|
||||
|
||||
@@ -8,22 +8,6 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) add customization for translations #857
|
||||
|
||||
### Changed
|
||||
|
||||
- 🧑💻(docker): add .next to .dockerignore #1055
|
||||
|
||||
### Fixed
|
||||
|
||||
-🐛(frontend) table of content disappearing #982
|
||||
-🐛(frontend) fix multiple EmojiPicker #1012
|
||||
-🐛(frontend) fix meta title #1017
|
||||
-🔧(git) set LF line endings for all text files #1032
|
||||
|
||||
|
||||
## [3.3.0] - 2025-05-06
|
||||
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
web: bin/buildpack_start.sh
|
||||
postdeploy: python manage.py migrate
|
||||
@@ -57,7 +57,7 @@ Available methods: Helm chart, Nix package
|
||||
|
||||
In the works: Docker Compose, YunoHost
|
||||
|
||||
⚠️ For some advanced features (ex: Export as PDF) Docs relies on XL packages from BlockNote. These are licenced under AGPL-3.0 and are not MIT compatible. You can perfectly use Docs without these packages by setting the environment variable `PUBLISH_AS_MIT` to true. That way you'll build an image of the application without the features that are not MIT compatible. Read the [environment variables documentation](/docs/env.md) for more information.
|
||||
⚠️ For some advanced features (ex: Export as PDF) Docs relies on XL packages from BlockNote. These are licenced under AGPL-3.0 and are not MIT compatible. You can perfectly use Docs without these packages by setting the environment variable `PUBLISH_AS_MIT` to true. That way you'll build an image of the application without the features that are not MIT compatible. Read the [environment variables documentation](/docs/docs/env.md) for more information.
|
||||
|
||||
## Getting started 🔧
|
||||
|
||||
@@ -97,7 +97,7 @@ This command builds the `app` container, installs dependencies, performs databas
|
||||
|
||||
Your Docker services should now be up and running 🎉
|
||||
|
||||
You can access the project by going to <http://localhost:3000>.
|
||||
You can access to the project by going to <http://localhost:3000>.
|
||||
|
||||
You will be prompted to log in. The default credentials are:
|
||||
|
||||
@@ -106,7 +106,7 @@ username: impress
|
||||
password: impress
|
||||
```
|
||||
|
||||
📝 Note that if you need to run them afterwards, you can use the eponymous Make rule:
|
||||
📝 Note that if you need to run them afterwards, you can use the eponym Make rule:
|
||||
|
||||
```shellscript
|
||||
$ make run
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ the following command inside your docker container:
|
||||
|
||||
## [3.3.0] - 2025-05-22
|
||||
|
||||
⚠️ For some advanced features (ex: Export as PDF) Docs relies on XL packages from BlockNote. These are licenced under AGPL-3.0 and are not MIT compatible. You can perfectly use Docs without these packages by setting the environment variable `PUBLISH_AS_MIT` to true. That way you'll build an image of the application without the features that are not MIT compatible. Read the [environment variables documentation](/docs/env.md) for more information.
|
||||
⚠️ For some advanced features (ex: Export as PDF) Docs relies on XL packages from BlockNote. These are licenced under AGPL-3.0 and are not MIT compatible. You can perfectly use Docs without these packages by setting the environment variable `PUBLISH_AS_MIT` to true. That way you'll build an image of the application without the features that are not MIT compatible. Read the [environment variables documentation](/docs/docs/env.md) for more information.
|
||||
|
||||
The footer is now configurable from a customization file. To override the default one, you can
|
||||
use the `THEME_CUSTOMIZATION_FILE_PATH` environment variable to point to your customization file.
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit # always exit on error
|
||||
set -o pipefail # don't ignore exit codes when piping output
|
||||
|
||||
echo "-----> Running post-compile script"
|
||||
|
||||
rm -rf docker docs env.d gitlint src/frontend/apps/e2e
|
||||
rm -rf src/frontend/apps
|
||||
rm -rf src/frontend/packages
|
||||
|
||||
# Remove some of the larger packages required by the frontend only
|
||||
rm -rf src/frontend/node_modules/@next src/frontend/node_modules/next src/frontend/node_modules/react-icons src/frontend/node_modules/@gouvfr-lasuite
|
||||
|
||||
# du -ch | sort -rh | head -n 100
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit # always exit on error
|
||||
set -o pipefail # don't ignore exit codes when piping output
|
||||
|
||||
echo "-----> Running post-frontend script"
|
||||
|
||||
# Move the frontend build to the nginx root and clean up
|
||||
mkdir -p build/
|
||||
mv src/frontend/apps/impress/out build/frontend-out
|
||||
|
||||
mv src/backend/* ./
|
||||
mv src/nginx/* ./
|
||||
|
||||
echo "3.13" > .python-version
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Start the Django backend server
|
||||
gunicorn -b :8000 impress.wsgi:application --log-file - &
|
||||
|
||||
# Start the Y provider service
|
||||
cd src/frontend/servers/y-provider && PORT=4444 ../../.scalingo/node/bin/node dist/start-server.js &
|
||||
|
||||
# Start the Nginx server
|
||||
bin/run &
|
||||
|
||||
# if the current shell is killed, also terminate all its children
|
||||
trap "pkill SIGTERM -P $$" SIGTERM
|
||||
|
||||
# wait for a single child to finish,
|
||||
wait -n
|
||||
# then kill all the other tasks
|
||||
pkill -P $$
|
||||
+1
-1
@@ -171,7 +171,7 @@ services:
|
||||
working_dir: /app
|
||||
|
||||
node:
|
||||
image: node:22
|
||||
image: node:18
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
environment:
|
||||
HOME: /tmp
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ These are the environment variables you can set for the `impress-backend` contai
|
||||
| DJANGO_EMAIL_USE_TLS | use tls for email host connection | false |
|
||||
| DJANGO_EMAIL_USE_SSL | use sstl for email host connection | false |
|
||||
| DJANGO_EMAIL_FROM | email address used as sender | from@example.com |
|
||||
| DJANGO_CORS_ALLOW_ALL_ORIGINS | allow all CORS origins | false |
|
||||
| DJANGO_CORS_ALLOW_ALL_ORIGINS | allow all CORS origins | true |
|
||||
| DJANGO_CORS_ALLOWED_ORIGINS | list of origins allowed for CORS | [] |
|
||||
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | list of origins allowed for CORS using regulair expressions | [] |
|
||||
| SENTRY_DSN | sentry host | |
|
||||
|
||||
@@ -53,18 +53,4 @@ Below is a visual example of a configured footer ⬇️:
|
||||
|
||||

|
||||
|
||||
----
|
||||
|
||||
# **Custom Translations** 📝
|
||||
|
||||
The translations can be partially overridden from the theme customization file.
|
||||
|
||||
### Settings 🔧
|
||||
|
||||
```shellscript
|
||||
THEME_CUSTOMIZATION_FILE_PATH=<path>
|
||||
```
|
||||
|
||||
### Example of JSON
|
||||
|
||||
The json must follow some rules: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": ["github>numerique-gouv/renovate-configuration"],
|
||||
"dependencyDashboard": true,
|
||||
"labels": ["dependencies", "noChangeLog", "automated"],
|
||||
"labels": ["dependencies", "noChangeLog"],
|
||||
"packageRules": [
|
||||
{
|
||||
"enabled": false,
|
||||
|
||||
@@ -404,7 +404,7 @@ class DocumentViewSet(
|
||||
|
||||
Example:
|
||||
- Ascending: GET /api/v1.0/documents/?ordering=created_at
|
||||
- Descending: GET /api/v1.0/documents/?ordering=-title
|
||||
- Desceding: GET /api/v1.0/documents/?ordering=-title
|
||||
|
||||
### Filtering:
|
||||
- `is_creator_me=true`: Returns documents created by the current user.
|
||||
|
||||
@@ -747,7 +747,7 @@ class Document(MP_Node, BaseModel):
|
||||
for ancestor in ancestors_links:
|
||||
links_definitions[ancestor["link_reach"]].add(ancestor["link_role"])
|
||||
|
||||
return dict(links_definitions) # Convert default dict back to a normal dict
|
||||
return dict(links_definitions) # Convert defaultdict back to a normal dict
|
||||
|
||||
def compute_ancestors_links(self, user):
|
||||
"""
|
||||
|
||||
@@ -16,6 +16,7 @@ from socket import gethostbyname, gethostname
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import dj_database_url
|
||||
import sentry_sdk
|
||||
from configurations import Configuration, values
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
@@ -74,7 +75,9 @@ class Base(Configuration):
|
||||
|
||||
# Database
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"default": dj_database_url.config()
|
||||
if os.environ.get("DATABASE_URL")
|
||||
else {
|
||||
"ENGINE": values.Value(
|
||||
"django.db.backends.postgresql_psycopg2",
|
||||
environ_name="DB_ENGINE",
|
||||
@@ -396,7 +399,7 @@ class Base(Configuration):
|
||||
|
||||
# CORS
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(False)
|
||||
CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(True)
|
||||
CORS_ALLOWED_ORIGINS = values.ListValue([])
|
||||
CORS_ALLOWED_ORIGIN_REGEXES = values.ListValue([])
|
||||
|
||||
|
||||
+16
-15
@@ -26,9 +26,10 @@ readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"beautifulsoup4==4.13.4",
|
||||
"boto3==1.38.33",
|
||||
"boto3==1.38.18",
|
||||
"Brotli==1.1.0",
|
||||
"celery[redis]==5.5.3",
|
||||
"celery[redis]==5.5.2",
|
||||
"dj-database-url==2.3.0",
|
||||
"django-configurations==2.5.1",
|
||||
"django-cors-headers==4.7.0",
|
||||
"django-countries==7.6.1",
|
||||
@@ -38,7 +39,7 @@ dependencies = [
|
||||
"django-redis==5.4.0",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django==5.1.10",
|
||||
"django==5.1.9",
|
||||
"django-treebeard==4.7.1",
|
||||
"djangorestframework==3.16.0",
|
||||
"drf_spectacular==0.28.0",
|
||||
@@ -46,19 +47,19 @@ dependencies = [
|
||||
"easy_thumbnails==2.10",
|
||||
"factory_boy==3.3.3",
|
||||
"gunicorn==23.0.0",
|
||||
"jsonschema==4.24.0",
|
||||
"jsonschema==4.23.0",
|
||||
"lxml==5.4.0",
|
||||
"markdown==3.8",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
"nested-multipart-parser==1.5.0",
|
||||
"openai==1.85.0",
|
||||
"openai==1.79.0",
|
||||
"psycopg[binary]==3.2.9",
|
||||
"pycrdt==0.12.20",
|
||||
"pycrdt==0.12.19",
|
||||
"PyJWT==2.10.1",
|
||||
"python-magic==0.4.27",
|
||||
"redis<6.0.0",
|
||||
"requests==2.32.4",
|
||||
"sentry-sdk==2.29.1",
|
||||
"requests==2.32.3",
|
||||
"sentry-sdk==2.28.0",
|
||||
"whitenoise==6.9.0",
|
||||
]
|
||||
|
||||
@@ -72,21 +73,21 @@ dependencies = [
|
||||
dev = [
|
||||
"django-extensions==4.1",
|
||||
"django-test-migrations==1.5.0",
|
||||
"drf-spectacular-sidecar==2025.6.1",
|
||||
"freezegun==1.5.2",
|
||||
"drf-spectacular-sidecar==2025.5.1",
|
||||
"freezegun==1.5.1",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==9.3.0",
|
||||
"ipython==9.2.0",
|
||||
"pyfakefs==5.8.0",
|
||||
"pylint-django==2.6.1",
|
||||
"pylint==3.3.7",
|
||||
"pytest-cov==6.1.1",
|
||||
"pytest-django==4.11.1",
|
||||
"pytest==8.4.0",
|
||||
"pytest==8.3.5",
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.7.0",
|
||||
"pytest-xdist==3.6.1",
|
||||
"responses==0.25.7",
|
||||
"ruff==0.11.13",
|
||||
"types-requests==2.32.0.20250602",
|
||||
"ruff==0.11.10",
|
||||
"types-requests==2.32.0.20250515",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
|
||||
@@ -119,29 +119,9 @@ test.describe('Config', () => {
|
||||
.first(),
|
||||
).toBeAttached();
|
||||
});
|
||||
|
||||
test('it checks theme_customization.translations config', async ({
|
||||
page,
|
||||
}) => {
|
||||
await overrideConfig(page, {
|
||||
theme_customization: {
|
||||
translations: {
|
||||
en: {
|
||||
translation: {
|
||||
Docs: 'MyCustomDocs',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page.getByText('MyCustomDocs')).toBeAttached();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Config: Not logged', () => {
|
||||
test.describe('Config: Not loggued', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('it checks the config api is called', async ({ page }) => {
|
||||
|
||||
@@ -31,7 +31,7 @@ test.describe('Doc Create', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Doc Create: Not logged', () => {
|
||||
test.describe('Doc Create: Not loggued', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('it creates a doc server way', async ({
|
||||
|
||||
@@ -95,7 +95,7 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
const selectVisibility = page.getByLabel('Visibility', { exact: true });
|
||||
|
||||
// When the visibility is changed, the ws should close the connection (backend signal)
|
||||
// When the visibility is changed, the ws should closed the connection (backend signal)
|
||||
const wsClosePromise = webSocket.waitForEvent('close');
|
||||
|
||||
await selectVisibility.click();
|
||||
|
||||
@@ -270,7 +270,7 @@ test.describe('Doc Export', () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* We cannot assert the line break is visible in the pdf, but we can assert the
|
||||
* We cannot assert the line break is visible in the pdf but we can assert the
|
||||
* line break is visible in the editor and that the pdf is generated.
|
||||
*/
|
||||
test('it exports the doc with divider', async ({ page, browserName }) => {
|
||||
|
||||
@@ -96,7 +96,7 @@ test.describe('Doc Routing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Doc Routing: Not logged', () => {
|
||||
test.describe('Doc Routing: Not loggued', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('checks redirect to a doc after login', async ({
|
||||
|
||||
@@ -295,7 +295,7 @@ test.describe('Doc Visibility: Public', () => {
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByLabel('Visibility mode').click();
|
||||
await page.getByLabel('Editing').click();
|
||||
await page.getByLabel('Edition').click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document visibility has been updated.').first(),
|
||||
@@ -333,7 +333,7 @@ test.describe('Doc Visibility: Public', () => {
|
||||
test.describe('Doc Visibility: Authenticated', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('A doc is not accessible when unauthenticated.', async ({
|
||||
test('A doc is not accessible when unauthentified.', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
@@ -476,7 +476,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
|
||||
const urlDoc = page.url();
|
||||
await page.getByLabel('Visibility mode').click();
|
||||
await page.getByLabel('Editing').click();
|
||||
await page.getByLabel('Edition').click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document visibility has been updated.').first(),
|
||||
|
||||
@@ -128,16 +128,8 @@ export async function waitForLanguageSwitch(
|
||||
lang: TestLanguageValue,
|
||||
) {
|
||||
const header = page.locator('header').first();
|
||||
const languagePicker = header.locator('.--docs--language-picker-text');
|
||||
const isAlreadyTargetLanguage = await languagePicker
|
||||
.innerText()
|
||||
.then((text) => text.toLowerCase().includes(lang.label.toLowerCase()));
|
||||
await header.getByRole('button', { name: 'arrow_drop_down' }).click();
|
||||
|
||||
if (isAlreadyTargetLanguage) {
|
||||
return;
|
||||
}
|
||||
|
||||
await languagePicker.click();
|
||||
const responsePromise = page.waitForResponse(
|
||||
(resp) =>
|
||||
resp.url().includes('/user') && resp.request().method() === 'PATCH',
|
||||
|
||||
@@ -16,56 +16,56 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ag-media/react-pdf-table": "2.0.3",
|
||||
"@blocknote/code-block": "0.31.1",
|
||||
"@blocknote/core": "0.31.1",
|
||||
"@blocknote/mantine": "0.31.1",
|
||||
"@blocknote/react": "0.31.1",
|
||||
"@blocknote/xl-docx-exporter": "0.31.1",
|
||||
"@blocknote/xl-pdf-exporter": "0.31.1",
|
||||
"@blocknote/code-block": "0.30.1",
|
||||
"@blocknote/core": "0.30.1",
|
||||
"@blocknote/mantine": "0.30.1",
|
||||
"@blocknote/react": "0.30.1",
|
||||
"@blocknote/xl-docx-exporter": "0.30.1",
|
||||
"@blocknote/xl-pdf-exporter": "0.30.1",
|
||||
"@emoji-mart/data": "1.2.1",
|
||||
"@emoji-mart/react": "1.1.1",
|
||||
"@fontsource/material-icons": "5.2.5",
|
||||
"@gouvfr-lasuite/integration": "1.0.3",
|
||||
"@gouvfr-lasuite/ui-kit": "0.7.0",
|
||||
"@gouvfr-lasuite/ui-kit": "0.6.0",
|
||||
"@hocuspocus/provider": "2.15.2",
|
||||
"@openfun/cunningham-react": "3.1.0",
|
||||
"@react-pdf/renderer": "4.3.0",
|
||||
"@sentry/nextjs": "9.26.0",
|
||||
"@tanstack/react-query": "5.80.5",
|
||||
"@sentry/nextjs": "9.19.0",
|
||||
"@tanstack/react-query": "5.76.1",
|
||||
"canvg": "4.0.3",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"docx": "9.5.0",
|
||||
"emoji-mart": "5.6.0",
|
||||
"i18next": "25.2.1",
|
||||
"i18next": "25.1.3",
|
||||
"i18next-browser-languagedetector": "8.1.0",
|
||||
"idb": "8.0.3",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.6.1",
|
||||
"next": "15.3.3",
|
||||
"posthog-js": "1.249.3",
|
||||
"next": "15.3.2",
|
||||
"posthog-js": "1.242.2",
|
||||
"react": "*",
|
||||
"react-aria-components": "1.9.0",
|
||||
"react-aria-components": "1.8.0",
|
||||
"react-dom": "*",
|
||||
"react-i18next": "15.5.2",
|
||||
"react-i18next": "15.5.1",
|
||||
"react-intersection-observer": "9.16.0",
|
||||
"react-select": "5.10.1",
|
||||
"styled-components": "6.1.18",
|
||||
"use-debounce": "10.0.4",
|
||||
"y-protocols": "1.0.6",
|
||||
"yjs": "*",
|
||||
"zustand": "5.0.5"
|
||||
"zustand": "5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@tanstack/react-query-devtools": "5.80.5",
|
||||
"@tanstack/react-query-devtools": "5.76.1",
|
||||
"@testing-library/dom": "10.4.0",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lodash": "4.17.17",
|
||||
"@types/lodash": "4.17.16",
|
||||
"@types/luxon": "3.6.2",
|
||||
"@types/node": "*",
|
||||
"@types/react": "*",
|
||||
@@ -78,11 +78,11 @@
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"node-fetch": "2.7.0",
|
||||
"prettier": "3.5.3",
|
||||
"stylelint": "16.20.0",
|
||||
"stylelint": "16.19.1",
|
||||
"stylelint-config-standard": "38.0.0",
|
||||
"stylelint-prettier": "5.0.3",
|
||||
"typescript": "*",
|
||||
"webpack": "5.99.9",
|
||||
"webpack": "5.99.8",
|
||||
"workbox-webpack-plugin": "7.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('fetchAPI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('check the versioning', () => {
|
||||
it('check the versionning', () => {
|
||||
fetchMock.mock('http://test.jest/api/v2.0/some/url', 200);
|
||||
|
||||
void fetchAPI('some/url', {}, '2.0');
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PropsWithChildren, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components';
|
||||
@@ -39,7 +38,6 @@ export const DropdownMenu = ({
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const blockButtonRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onOpenChange = (isOpen: boolean) => {
|
||||
setIsOpen(isOpen);
|
||||
@@ -152,7 +150,6 @@ export const DropdownMenu = ({
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap={spacingsTokens['base']}
|
||||
aria-selected={option.isSelected ? true : undefined}
|
||||
>
|
||||
{option.icon && (
|
||||
<Icon
|
||||
@@ -167,14 +164,7 @@ export const DropdownMenu = ({
|
||||
</Text>
|
||||
</Box>
|
||||
{option.isSelected && (
|
||||
<Icon
|
||||
role="img"
|
||||
iconName="check"
|
||||
$size="20px"
|
||||
$theme="greyscale"
|
||||
aria-label={t('Checked')}
|
||||
aria-hidden="false"
|
||||
/>
|
||||
<Icon iconName="check" $size="20px" $theme="greyscale" />
|
||||
)}
|
||||
</BoxButton>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,6 @@ export const Icon = ({
|
||||
}: IconProps) => {
|
||||
return (
|
||||
<Text
|
||||
aria-hidden="true"
|
||||
{...textProps}
|
||||
className={clsx('--docs--icon-bg', textProps.className, {
|
||||
'material-icons-filled': variant === 'filled',
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import {
|
||||
UseMutationResult,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { User } from '@/features/auth/api/types';
|
||||
import { KEY_AUTH } from '@/features/auth/api/useAuthQuery';
|
||||
|
||||
type UserUpdateRequest = Partial<User>;
|
||||
|
||||
async function updateUser(userUpdateData: UserUpdateRequest): Promise<User> {
|
||||
const response = await fetchAPI(`users/${userUpdateData.id}/`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(userUpdateData),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
`Failed to update the user`,
|
||||
await errorCauses(response, userUpdateData),
|
||||
);
|
||||
}
|
||||
return response.json() as Promise<User>;
|
||||
}
|
||||
|
||||
export const useUserUpdate = (): UseMutationResult<
|
||||
User,
|
||||
APIError,
|
||||
UserUpdateRequest
|
||||
> => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutationResult = useMutation<User, APIError, UserUpdateRequest>({
|
||||
mutationFn: updateUser,
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: [KEY_AUTH] });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error updating user', error);
|
||||
},
|
||||
});
|
||||
|
||||
return mutationResult;
|
||||
};
|
||||
@@ -1,15 +1,10 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import Head from 'next/head';
|
||||
import { PropsWithChildren, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PropsWithChildren, useEffect } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useAuthQuery } from '@/features/auth';
|
||||
import {
|
||||
useCustomTranslations,
|
||||
useSynchronizedLanguage,
|
||||
} from '@/features/language';
|
||||
import { useLanguageSynchronizer } from '@/features/language/';
|
||||
import { useAnalytics } from '@/libs';
|
||||
import { CrispProvider, PostHogAnalytic } from '@/services';
|
||||
import { useSentryStore } from '@/stores/useSentryStore';
|
||||
@@ -18,35 +13,10 @@ import { useConfig } from './api/useConfig';
|
||||
|
||||
export const ConfigProvider = ({ children }: PropsWithChildren) => {
|
||||
const { data: conf } = useConfig();
|
||||
const { data: user } = useAuthQuery();
|
||||
const { setSentry } = useSentryStore();
|
||||
const { setTheme } = useCunninghamTheme();
|
||||
const { changeLanguageSynchronized } = useSynchronizedLanguage();
|
||||
const { customizeTranslations } = useCustomTranslations();
|
||||
const { AnalyticsProvider } = useAnalytics();
|
||||
const { i18n } = useTranslation();
|
||||
const languageSynchronized = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || languageSynchronized.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetLanguage =
|
||||
user?.language ?? i18n.resolvedLanguage ?? i18n.language;
|
||||
|
||||
void changeLanguageSynchronized(targetLanguage, user).then(() => {
|
||||
languageSynchronized.current = true;
|
||||
});
|
||||
}, [user, i18n.resolvedLanguage, i18n.language, changeLanguageSynchronized]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!conf?.theme_customization?.translations) {
|
||||
return;
|
||||
}
|
||||
|
||||
customizeTranslations(conf.theme_customization.translations);
|
||||
}, [conf?.theme_customization?.translations, customizeTranslations]);
|
||||
const { synchronizeLanguage } = useLanguageSynchronizer();
|
||||
|
||||
useEffect(() => {
|
||||
if (!conf?.SENTRY_DSN) {
|
||||
@@ -64,6 +34,10 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
|
||||
setTheme(conf.FRONTEND_THEME);
|
||||
}, [conf?.FRONTEND_THEME, setTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
void synchronizeLanguage();
|
||||
}, [synchronizeLanguage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!conf?.POSTHOG_KEY) {
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Resource } from 'i18next';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { Theme } from '@/cunningham/';
|
||||
@@ -8,10 +7,9 @@ import { PostHogConf } from '@/services';
|
||||
|
||||
interface ThemeCustomization {
|
||||
footer?: FooterType;
|
||||
translations?: Resource;
|
||||
}
|
||||
|
||||
export interface ConfigResponse {
|
||||
interface ConfigResponse {
|
||||
AI_FEATURE_ENABLED?: boolean;
|
||||
COLLABORATION_WS_URL?: string;
|
||||
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY?: boolean;
|
||||
|
||||
@@ -11,5 +11,5 @@ export interface User {
|
||||
email: string;
|
||||
full_name: string;
|
||||
short_name: string;
|
||||
language?: string;
|
||||
language: string;
|
||||
}
|
||||
|
||||
@@ -178,6 +178,7 @@ export const BlockNoteEditorVersion = ({
|
||||
initialContent,
|
||||
}: BlockNoteEditorVersionProps) => {
|
||||
const readOnly = true;
|
||||
const { setEditor } = useEditorStore();
|
||||
const editor = useCreateBlockNote(
|
||||
{
|
||||
collaboration: {
|
||||
@@ -192,6 +193,15 @@ export const BlockNoteEditorVersion = ({
|
||||
},
|
||||
[initialContent],
|
||||
);
|
||||
useHeadings(editor);
|
||||
|
||||
useEffect(() => {
|
||||
setEditor(editor);
|
||||
|
||||
return () => {
|
||||
setEditor(undefined);
|
||||
};
|
||||
}, [setEditor, editor]);
|
||||
|
||||
return (
|
||||
<Box $css={cssEditor(readOnly)} className="--docs--editor-container">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { css } from 'styled-components';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { Box, Text, TextErrors } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { DocHeader, DocVersionHeader } from '@/docs/doc-header/';
|
||||
import {
|
||||
Doc,
|
||||
@@ -25,6 +26,9 @@ interface DocEditorProps {
|
||||
export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const isVersion = !!versionId && typeof versionId === 'string';
|
||||
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const { provider } = useProviderStore();
|
||||
|
||||
if (!provider) {
|
||||
@@ -62,6 +66,7 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
$background={colorsTokens['primary-bg']}
|
||||
$direction="row"
|
||||
$width="100%"
|
||||
$css="overflow-x: clip; flex: 1;"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EmojiMartData } from '@emoji-mart/data';
|
||||
import data from '@emoji-mart/data';
|
||||
import Picker from '@emoji-mart/react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -6,15 +6,19 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Box } from '@/components';
|
||||
|
||||
interface EmojiPickerProps {
|
||||
emojiData: EmojiMartData;
|
||||
categories: string[];
|
||||
custom: {
|
||||
name: string;
|
||||
id: string;
|
||||
emojis: string[];
|
||||
}[];
|
||||
onClickOutside: () => void;
|
||||
onEmojiSelect: ({ native }: { native: string }) => void;
|
||||
}
|
||||
|
||||
export const EmojiPicker = ({
|
||||
emojiData,
|
||||
categories,
|
||||
custom,
|
||||
onClickOutside,
|
||||
onEmojiSelect,
|
||||
}: EmojiPickerProps) => {
|
||||
@@ -23,8 +27,9 @@ export const EmojiPicker = ({
|
||||
return (
|
||||
<Box $position="absolute" $zIndex={1000} $margin="2rem 0 0 0">
|
||||
<Picker
|
||||
data={emojiData}
|
||||
categories={categories}
|
||||
custom={custom}
|
||||
data={data}
|
||||
locale={i18n.resolvedLanguage}
|
||||
navPosition="none"
|
||||
onClickOutside={onClickOutside}
|
||||
|
||||
+48
-3
@@ -10,7 +10,52 @@ import { Box, BoxButton, Icon } from '@/components';
|
||||
import { DocsBlockNoteEditor } from '../../types';
|
||||
import { EmojiPicker } from '../EmojiPicker';
|
||||
|
||||
import InitEmojiCallout from './initEmojiCallout';
|
||||
const calloutCustom = [
|
||||
{
|
||||
name: 'Callout',
|
||||
id: 'callout',
|
||||
emojis: [
|
||||
'bulb',
|
||||
'point_right',
|
||||
'point_up',
|
||||
'ok_hand',
|
||||
'key',
|
||||
'construction',
|
||||
'warning',
|
||||
'fire',
|
||||
'pushpin',
|
||||
'scissors',
|
||||
'question',
|
||||
'no_entry',
|
||||
'no_entry_sign',
|
||||
'alarm_clock',
|
||||
'phone',
|
||||
'rotating_light',
|
||||
'recycle',
|
||||
'white_check_mark',
|
||||
'lock',
|
||||
'paperclip',
|
||||
'book',
|
||||
'speaking_head_in_silhouette',
|
||||
'arrow_right',
|
||||
'loudspeaker',
|
||||
'hammer_and_wrench',
|
||||
'gear',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const calloutCategories = [
|
||||
'callout',
|
||||
'people',
|
||||
'nature',
|
||||
'foods',
|
||||
'activity',
|
||||
'places',
|
||||
'flags',
|
||||
'objects',
|
||||
'symbols',
|
||||
];
|
||||
|
||||
export const CalloutBlock = createReactBlockSpec(
|
||||
{
|
||||
@@ -79,8 +124,8 @@ export const CalloutBlock = createReactBlockSpec(
|
||||
|
||||
{openEmojiPicker && (
|
||||
<EmojiPicker
|
||||
emojiData={InitEmojiCallout.emojidata}
|
||||
categories={InitEmojiCallout.calloutCategories}
|
||||
categories={calloutCategories}
|
||||
custom={calloutCustom}
|
||||
onClickOutside={onClickOutside}
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
/>
|
||||
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* "emoji-mart" is a singleton, multiple imports in the same
|
||||
* application could cause issues.
|
||||
* BlockNote uses "emoji-mart" internally as well, if
|
||||
* Blocknote emoji picker is init before the callout emoji picker,
|
||||
* the callout emoji picker will not be set up correctly.
|
||||
* To avoid this, we initialize emoji-mart here and before any
|
||||
* other components that uses it.
|
||||
*/
|
||||
import data, { Category, EmojiMartData } from '@emoji-mart/data';
|
||||
import { init } from 'emoji-mart';
|
||||
|
||||
type EmojiMartDataFixed = Omit<EmojiMartData, 'categories'> & {
|
||||
categories: (Category & { name: string })[];
|
||||
};
|
||||
|
||||
const emojidata = structuredClone(data) as EmojiMartDataFixed;
|
||||
|
||||
const CALLOUT_ID = 'callout';
|
||||
const CALLOUT_EMOJIS = [
|
||||
'bulb',
|
||||
'point_right',
|
||||
'point_up',
|
||||
'ok_hand',
|
||||
'key',
|
||||
'construction',
|
||||
'warning',
|
||||
'fire',
|
||||
'pushpin',
|
||||
'scissors',
|
||||
'question',
|
||||
'no_entry',
|
||||
'no_entry_sign',
|
||||
'alarm_clock',
|
||||
'phone',
|
||||
'rotating_light',
|
||||
'recycle',
|
||||
'white_check_mark',
|
||||
'lock',
|
||||
'paperclip',
|
||||
'book',
|
||||
'speaking_head_in_silhouette',
|
||||
'arrow_right',
|
||||
'loudspeaker',
|
||||
'hammer_and_wrench',
|
||||
'gear',
|
||||
];
|
||||
|
||||
if (!emojidata.categories.some((c) => c.id === CALLOUT_ID)) {
|
||||
emojidata.categories.unshift({
|
||||
id: CALLOUT_ID,
|
||||
name: 'Callout',
|
||||
emojis: CALLOUT_EMOJIS,
|
||||
});
|
||||
}
|
||||
|
||||
void init({ data: emojidata });
|
||||
|
||||
const calloutCategories = [
|
||||
'callout',
|
||||
'people',
|
||||
'nature',
|
||||
'foods',
|
||||
'activity',
|
||||
'places',
|
||||
'flags',
|
||||
'objects',
|
||||
'symbols',
|
||||
];
|
||||
|
||||
const calloutEmojiData = {
|
||||
emojidata,
|
||||
calloutCategories,
|
||||
};
|
||||
|
||||
export default calloutEmojiData;
|
||||
@@ -9,7 +9,7 @@ export const useHeadings = (editor: DocsBlockNoteEditor) => {
|
||||
useEffect(() => {
|
||||
setHeadings(editor);
|
||||
|
||||
editor?.onChange(() => {
|
||||
editor?.onEditorContentChange(() => {
|
||||
setHeadings(editor);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ export const cssEditor = (readonly: boolean) => css`
|
||||
& > .bn-container,
|
||||
& .ProseMirror {
|
||||
height: 100%;
|
||||
padding-bottom: 2rem;
|
||||
|
||||
img.bn-visual-media[src*='-unsafe'] {
|
||||
pointer-events: none;
|
||||
@@ -73,7 +72,8 @@ export const cssEditor = (readonly: boolean) => css`
|
||||
border-radius: var(--c--theme--spacings--3xs);
|
||||
}
|
||||
|
||||
.bn-block[data-background-color] > .bn-block-content {
|
||||
.bn-block-content[data-content-type='paragraph'],
|
||||
.bn-block-content[data-content-type='heading'] {
|
||||
padding: var(--c--theme--spacings--3xs) var(--c--theme--spacings--3xs);
|
||||
border-radius: var(--c--theme--spacings--3xs);
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { DocsExporterPDF } from '../types';
|
||||
export const blockMappingParagraphPDF: DocsExporterPDF['mappings']['blockMapping']['paragraph'] =
|
||||
(block, exporter) => {
|
||||
/**
|
||||
* Break line in the editor are not rendered in the PDF
|
||||
* Breakline in the editor are not rendered in the PDF
|
||||
* By adding a space if the block is empty we ensure that the block is rendered
|
||||
*/
|
||||
if (Array.isArray(block.content)) {
|
||||
|
||||
@@ -34,12 +34,12 @@ export const useRemoveDoc = (options?: UseRemoveDocOptions) => {
|
||||
queryKey: [KEY_LIST_DOC],
|
||||
});
|
||||
if (options?.onSuccess) {
|
||||
void options.onSuccess(data, variables, context);
|
||||
options.onSuccess(data, variables, context);
|
||||
}
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
if (options?.onError) {
|
||||
void options.onError(error, variables, context);
|
||||
options.onError(error, variables, context);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -63,12 +63,12 @@ export const useDeleteDocAccess = (options?: UseDeleteDocAccessOptions) => {
|
||||
queryKey: [KEY_LIST_USER],
|
||||
});
|
||||
if (options?.onSuccess) {
|
||||
void options.onSuccess(data, variables, context);
|
||||
options.onSuccess(data, variables, context);
|
||||
}
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
if (options?.onError) {
|
||||
void options.onError(error, variables, context);
|
||||
options.onError(error, variables, context);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -58,12 +58,12 @@ export const useDeleteDocInvitation = (
|
||||
queryKey: [KEY_LIST_DOC_INVITATIONS],
|
||||
});
|
||||
if (options?.onSuccess) {
|
||||
void options.onSuccess(data, variables, context);
|
||||
options.onSuccess(data, variables, context);
|
||||
}
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
if (options?.onError) {
|
||||
void options.onError(error, variables, context);
|
||||
options.onError(error, variables, context);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -65,12 +65,12 @@ export const useUpdateDocAccess = (options?: UseUpdateDocAccessOptions) => {
|
||||
queryKey: [KEY_LIST_DOC],
|
||||
});
|
||||
if (options?.onSuccess) {
|
||||
void options.onSuccess(data, variables, context);
|
||||
options.onSuccess(data, variables, context);
|
||||
}
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
if (options?.onError) {
|
||||
void options.onError(error, variables, context);
|
||||
options.onError(error, variables, context);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -66,12 +66,12 @@ export const useUpdateDocInvitation = (
|
||||
queryKey: [KEY_LIST_DOC_INVITATIONS],
|
||||
});
|
||||
if (options?.onSuccess) {
|
||||
void options.onSuccess(data, variables, context);
|
||||
options.onSuccess(data, variables, context);
|
||||
}
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
if (options?.onError) {
|
||||
void options.onError(error, variables, context);
|
||||
options.onError(error, variables, context);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { Button } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, HorizontalSeparator, Icon } from '@/components';
|
||||
import { Box, HorizontalSeparator } from '@/components';
|
||||
import { Doc, useCopyDocLink } from '@/features/docs';
|
||||
|
||||
import { DocVisibility } from './DocVisibility';
|
||||
@@ -36,7 +36,7 @@ export const DocShareModalFooter = ({ doc, onClose }: Props) => {
|
||||
fullWidth={false}
|
||||
onClick={copyDocLink}
|
||||
color="tertiary"
|
||||
icon={<Icon iconName="add_link" />}
|
||||
icon={<span className="material-icons">add_link</span>}
|
||||
>
|
||||
{t('Copy link')}
|
||||
</Button>
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ export const useTranslatedShareSettings = () => {
|
||||
|
||||
const linkModeTranslations = {
|
||||
[LinkRole.READER]: t('Reading'),
|
||||
[LinkRole.EDITOR]: t('Editing'),
|
||||
[LinkRole.EDITOR]: t('Edition'),
|
||||
};
|
||||
|
||||
const linkReachChoices = {
|
||||
|
||||
@@ -40,7 +40,7 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
|
||||
$align="center"
|
||||
role="row"
|
||||
$gap="20px"
|
||||
$padding={{ vertical: '4xs', horizontal: isDesktop ? 'base' : 'xs' }}
|
||||
$padding={{ vertical: '2xs', horizontal: isDesktop ? 'base' : 'xs' }}
|
||||
$css={css`
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -50,7 +50,6 @@ export const SimpleDocItem = ({
|
||||
background-color: transparent;
|
||||
filter: drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.05));
|
||||
`}
|
||||
$padding={`${spacingsTokens['3xs']} 0`}
|
||||
>
|
||||
{isPinned ? (
|
||||
<PinnedDocumentIcon
|
||||
|
||||
+23
-14
@@ -1,33 +1,42 @@
|
||||
import { Settings } from 'luxon';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { DropdownMenu, Icon, Text } from '@/components/';
|
||||
import { useConfig } from '@/core';
|
||||
import { useAuthQuery } from '@/features/auth';
|
||||
import {
|
||||
getMatchingLocales,
|
||||
useSynchronizedLanguage,
|
||||
} from '@/features/language';
|
||||
|
||||
import { useLanguageSynchronizer } from './hooks/useLanguageSynchronizer';
|
||||
import { getMatchingLocales } from './utils/locale';
|
||||
|
||||
export const LanguagePicker = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { data: conf } = useConfig();
|
||||
const { data: user } = useAuthQuery();
|
||||
const { changeLanguageSynchronized } = useSynchronizedLanguage();
|
||||
const language = i18n.language;
|
||||
const { synchronizeLanguage } = useLanguageSynchronizer();
|
||||
const language = i18n.languages[0];
|
||||
Settings.defaultLocale = language;
|
||||
|
||||
// Compute options for dropdown
|
||||
const optionsPicker = useMemo(() => {
|
||||
const backendOptions = conf?.LANGUAGES ?? [[language, language]];
|
||||
return backendOptions.map(([backendLocale, backendLabel]) => {
|
||||
return {
|
||||
label: backendLabel,
|
||||
isSelected: getMatchingLocales([backendLocale], [language]).length > 0,
|
||||
callback: () => changeLanguageSynchronized(backendLocale, user),
|
||||
return backendOptions.map(([backendLocale, label]) => {
|
||||
// Determine if the option is selected
|
||||
const isSelected =
|
||||
getMatchingLocales([backendLocale], [language]).length > 0;
|
||||
// Define callback for updating both frontend and backend languages
|
||||
const callback = () => {
|
||||
i18n
|
||||
.changeLanguage(backendLocale)
|
||||
.then(() => {
|
||||
void synchronizeLanguage('toBackend');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Error changing language', err);
|
||||
});
|
||||
};
|
||||
return { label, isSelected, callback };
|
||||
});
|
||||
}, [changeLanguageSynchronized, conf?.LANGUAGES, language, user]);
|
||||
}, [conf, i18n, language, synchronizeLanguage]);
|
||||
|
||||
// Extract current language label for display
|
||||
const currentLanguageLabel =
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { User } from '@/features/auth/api/types';
|
||||
|
||||
export interface ChangeUserLanguageParams {
|
||||
userId: User['id'];
|
||||
language: User['language'];
|
||||
}
|
||||
|
||||
export const changeUserLanguage = async ({
|
||||
userId,
|
||||
language,
|
||||
}: ChangeUserLanguageParams): Promise<User> => {
|
||||
const response = await fetchAPI(`users/${userId}/`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
language,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
`Failed to change the user language to ${language}`,
|
||||
await errorCauses(response, {
|
||||
value: language,
|
||||
type: 'language',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return response.json() as Promise<User>;
|
||||
};
|
||||
|
||||
export function useChangeUserLanguage() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<User, APIError, ChangeUserLanguageParams>({
|
||||
mutationFn: changeUserLanguage,
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ['change-user-language'],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './LanguagePicker';
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './useSynchronizedLanguage';
|
||||
export * from './useCustomTranslations';
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Resource } from 'i18next';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const useCustomTranslations = () => {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
// Overwrite translations with a resource
|
||||
const customizeTranslations = useCallback(
|
||||
(currentCustomTranslations: Resource) => {
|
||||
Object.entries(currentCustomTranslations).forEach(([lng, namespaces]) => {
|
||||
Object.entries(namespaces).forEach(([ns, value]) => {
|
||||
i18n.addResourceBundle(lng, ns, value, true, true);
|
||||
});
|
||||
});
|
||||
// trigger re-render
|
||||
if (Object.entries(currentCustomTranslations).length > 0) {
|
||||
void i18n.changeLanguage(i18n.language);
|
||||
}
|
||||
},
|
||||
[i18n],
|
||||
);
|
||||
|
||||
return {
|
||||
customizeTranslations,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useConfig } from '@/core';
|
||||
import { useAuthQuery } from '@/features/auth/api';
|
||||
import { useChangeUserLanguage } from '@/features/language/api/useChangeUserLanguage';
|
||||
import { getMatchingLocales } from '@/features/language/utils/locale';
|
||||
import { availableFrontendLanguages } from '@/i18n/initI18n';
|
||||
|
||||
export const useLanguageSynchronizer = () => {
|
||||
const { data: conf, isSuccess: confInitialized } = useConfig();
|
||||
const { data: user, isSuccess: userInitialized } = useAuthQuery();
|
||||
const { i18n } = useTranslation();
|
||||
const { mutateAsync: changeUserLanguage } = useChangeUserLanguage();
|
||||
const languageSynchronizing = useRef(false);
|
||||
|
||||
const availableBackendLanguages = useMemo(() => {
|
||||
return conf?.LANGUAGES.map(([locale]) => locale);
|
||||
}, [conf?.LANGUAGES]);
|
||||
|
||||
const synchronizeLanguage = useCallback(
|
||||
async (direction?: 'toBackend' | 'toFrontend') => {
|
||||
if (
|
||||
languageSynchronizing.current ||
|
||||
!userInitialized ||
|
||||
!confInitialized ||
|
||||
!availableBackendLanguages ||
|
||||
!availableFrontendLanguages
|
||||
) {
|
||||
return;
|
||||
}
|
||||
languageSynchronizing.current = true;
|
||||
|
||||
try {
|
||||
const userPreferredLanguages = user.language ? [user.language] : [];
|
||||
const setOrDetectedLanguages = i18n.languages;
|
||||
|
||||
// Default direction depends on whether a user already has a language preference
|
||||
direction =
|
||||
direction ??
|
||||
(userPreferredLanguages.length ? 'toFrontend' : 'toBackend');
|
||||
|
||||
if (direction === 'toBackend') {
|
||||
// Update user's preference from frontends's language
|
||||
const closestBackendLanguage =
|
||||
getMatchingLocales(
|
||||
availableBackendLanguages,
|
||||
setOrDetectedLanguages,
|
||||
)[0] || availableBackendLanguages[0];
|
||||
await changeUserLanguage({
|
||||
userId: user.id,
|
||||
language: closestBackendLanguage,
|
||||
});
|
||||
} else {
|
||||
// Update frontends's language from user's preference
|
||||
const closestFrontendLanguage =
|
||||
getMatchingLocales(
|
||||
availableFrontendLanguages,
|
||||
userPreferredLanguages,
|
||||
)[0] || availableFrontendLanguages[0];
|
||||
if (i18n.resolvedLanguage !== closestFrontendLanguage) {
|
||||
await i18n.changeLanguage(closestFrontendLanguage);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error synchronizing language', error);
|
||||
} finally {
|
||||
languageSynchronizing.current = false;
|
||||
}
|
||||
},
|
||||
[
|
||||
i18n,
|
||||
user,
|
||||
userInitialized,
|
||||
confInitialized,
|
||||
availableBackendLanguages,
|
||||
changeUserLanguage,
|
||||
],
|
||||
);
|
||||
|
||||
return { synchronizeLanguage };
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useUserUpdate } from '@/core/api/useUserUpdate';
|
||||
import { useConfig } from '@/core/config/api/useConfig';
|
||||
import { User } from '@/features/auth';
|
||||
import { getMatchingLocales } from '@/features/language/utils/locale';
|
||||
|
||||
export const useSynchronizedLanguage = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const { mutateAsync: updateUser } = useUserUpdate();
|
||||
const { data: config } = useConfig();
|
||||
const isSynchronizingLanguage = useRef(false);
|
||||
|
||||
const availableFrontendLanguages = useMemo(
|
||||
() => Object.keys(i18n?.options?.resources || { en: '<- fallback' }),
|
||||
[i18n?.options?.resources],
|
||||
);
|
||||
const availableBackendLanguages = useMemo(
|
||||
() => config?.LANGUAGES?.map(([locale]) => locale) || [],
|
||||
[config?.LANGUAGES],
|
||||
);
|
||||
|
||||
const changeBackendLanguage = useCallback(
|
||||
async (language: string, user?: User) => {
|
||||
const closestBackendLanguage = getMatchingLocales(
|
||||
availableBackendLanguages,
|
||||
[language],
|
||||
)[0];
|
||||
|
||||
if (user && user.language !== closestBackendLanguage) {
|
||||
await updateUser({ id: user.id, language: closestBackendLanguage });
|
||||
}
|
||||
},
|
||||
[availableBackendLanguages, updateUser],
|
||||
);
|
||||
|
||||
const changeFrontendLanguage = useCallback(
|
||||
async (language: string) => {
|
||||
const closestFrontendLanguage = getMatchingLocales(
|
||||
availableFrontendLanguages,
|
||||
[language],
|
||||
)[0];
|
||||
if (
|
||||
i18n.isInitialized &&
|
||||
i18n.resolvedLanguage !== closestFrontendLanguage
|
||||
) {
|
||||
await i18n.changeLanguage(closestFrontendLanguage);
|
||||
}
|
||||
},
|
||||
[availableFrontendLanguages, i18n],
|
||||
);
|
||||
|
||||
const changeLanguageSynchronized = useCallback(
|
||||
async (language: string, user?: User) => {
|
||||
if (!isSynchronizingLanguage.current) {
|
||||
isSynchronizingLanguage.current = true;
|
||||
await changeFrontendLanguage(language);
|
||||
await changeBackendLanguage(language, user);
|
||||
isSynchronizingLanguage.current = false;
|
||||
}
|
||||
},
|
||||
[changeBackendLanguage, changeFrontendLanguage],
|
||||
);
|
||||
|
||||
return {
|
||||
changeLanguageSynchronized,
|
||||
changeFrontendLanguage,
|
||||
changeBackendLanguage,
|
||||
};
|
||||
};
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './hooks';
|
||||
export * from './components';
|
||||
export * from './utils';
|
||||
export * from './hooks/useLanguageSynchronizer';
|
||||
export * from './LanguagePicker';
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './locale';
|
||||
@@ -112,7 +112,7 @@ export class ApiPlugin implements WorkboxPlugin {
|
||||
};
|
||||
|
||||
/**
|
||||
* When we get a network error.
|
||||
* When we get an network error.
|
||||
*/
|
||||
handlerDidError: WorkboxPlugin['handlerDidError'] = async ({ request }) => {
|
||||
if (!this.isFetchDidFailed) {
|
||||
|
||||
@@ -4,38 +4,36 @@ import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import resources from './translations.json';
|
||||
|
||||
// Add an initialization guard
|
||||
let isInitialized = false;
|
||||
export const availableFrontendLanguages: readonly string[] =
|
||||
Object.keys(resources);
|
||||
|
||||
// Initialize i18next with the base translations only once
|
||||
if (!isInitialized && !i18next.isInitialized) {
|
||||
isInitialized = true;
|
||||
|
||||
i18next
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: 'en',
|
||||
debug: false,
|
||||
detection: {
|
||||
order: ['cookie', 'navigator'],
|
||||
caches: ['cookie'],
|
||||
lookupCookie: 'docs_language',
|
||||
cookieMinutes: 525600,
|
||||
cookieOptions: {
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
},
|
||||
i18next
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: 'en',
|
||||
debug: false,
|
||||
detection: {
|
||||
order: ['cookie', 'navigator'], // detection order
|
||||
caches: ['cookie'], // Use cookies to store the language preference
|
||||
lookupCookie: 'docs_language',
|
||||
cookieMinutes: 525600, // Expires after one year
|
||||
cookieOptions: {
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
},
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
lowerCaseLng: true,
|
||||
nsSeparator: false,
|
||||
keySeparator: false,
|
||||
})
|
||||
.catch((e) => console.error('i18n initialization failed:', e));
|
||||
}
|
||||
},
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
preload: availableFrontendLanguages,
|
||||
lowerCaseLng: true,
|
||||
nsSeparator: false,
|
||||
keySeparator: false,
|
||||
})
|
||||
.catch(() => {
|
||||
throw new Error('i18n initialization failed');
|
||||
});
|
||||
|
||||
export default i18next;
|
||||
|
||||
@@ -62,7 +62,6 @@
|
||||
"Beautify": "Verschönern",
|
||||
"Can't load this page, please check your internet connection.": "Diese Seite kann nicht geladen werden. Bitte überprüfen Sie Ihre Internetverbindung.",
|
||||
"Cancel": "Abbrechen",
|
||||
"Checked": "Angehakt",
|
||||
"Close the modal": "Pop up schliessen",
|
||||
"Collaborate": "Zusammenarbeiten",
|
||||
"Collaborate and write in real time, without layout constraints.": "In Echtzeit und ohne Layout-Beschränkungen schreiben und zusammenarbeiten.",
|
||||
@@ -254,7 +253,6 @@
|
||||
"Callout": "Destacado",
|
||||
"Can't load this page, please check your internet connection.": "No se puede cargar esta página, por favor compruebe su conexión a Internet.",
|
||||
"Cancel": "Cancelar",
|
||||
"Checked": "Marcado",
|
||||
"Close the modal": "Cerrar modal",
|
||||
"Collaborate": "Colabora",
|
||||
"Collaborate and write in real time, without layout constraints.": "Colaborar y escribir en tiempo real, sin restricciones de diseño.",
|
||||
@@ -439,7 +437,6 @@
|
||||
"Callout": "Alerte",
|
||||
"Can't load this page, please check your internet connection.": "Impossible de charger cette page, veuillez vérifier votre connexion Internet.",
|
||||
"Cancel": "Annuler",
|
||||
"Checked": "Coché",
|
||||
"Close the modal": "Fermer la modale",
|
||||
"Collaborate": "Collaborer",
|
||||
"Collaborate and write in real time, without layout constraints.": "Collaborez et rédigez en temps réel, sans contrainte de mise en page.",
|
||||
@@ -628,7 +625,6 @@
|
||||
"Beautify": "Filtro bellezza",
|
||||
"Can't load this page, please check your internet connection.": "Impossibile caricare questa pagina, controlla la tua connessione internet.",
|
||||
"Cancel": "Cancella",
|
||||
"Checked": "Selezionato",
|
||||
"Collaborate": "Collabora",
|
||||
"Collaborate and write in real time, without layout constraints.": "Collaborare e scrivere in tempo reale, senza vincoli di layout.",
|
||||
"Collaborative writing, Simplified.": "Scrittura collaborativa, semplificata.",
|
||||
@@ -785,7 +781,6 @@
|
||||
"Beautify": "Maak mooier",
|
||||
"Can't load this page, please check your internet connection.": "Kan deze pagina niet laden. Controleer je internetverbinding.",
|
||||
"Cancel": "Breek af",
|
||||
"Checked": "Aangevinkt",
|
||||
"Close the modal": "Sluit het venster",
|
||||
"Collaborate": "Samenwerken",
|
||||
"Collaborate and write in real time, without layout constraints.": "Samenwerken en schrijven in realtime, zonder lay-out beperkingen.",
|
||||
@@ -957,7 +952,6 @@
|
||||
"Anonymous": "Anonym",
|
||||
"Beautify": "Försköna",
|
||||
"Cancel": "Avbryt",
|
||||
"Checked": "Markerad",
|
||||
"Close the modal": "Stäng fönstret",
|
||||
"Convert Markdown": "Konvertera Markdown",
|
||||
"Correct": "Korrekt",
|
||||
@@ -995,7 +989,6 @@
|
||||
"Banner image": "Afiş görseli",
|
||||
"Beautify": "Güzelleştir",
|
||||
"Cancel": "İptal",
|
||||
"Checked": "İşaretli",
|
||||
"Collaborate": "İşbirliği Yapın",
|
||||
"Collaborate and write in real time, without layout constraints.": "Düzen kısıtlamaları olmadan gerçek zamanlı olarak işbirliği yapın ve yazın.",
|
||||
"Collaborative writing, Simplified.": "İş birliğiyle yazmak çok daha Kolay.",
|
||||
@@ -1085,7 +1078,6 @@
|
||||
"Callout": "标注",
|
||||
"Can't load this page, please check your internet connection.": "无法加载此页面,请检查您的网络连接。",
|
||||
"Cancel": "取消",
|
||||
"Checked": "已勾选",
|
||||
"Close the modal": "关闭对话框",
|
||||
"Collaborate": "协作",
|
||||
"Collaborate and write in real time, without layout constraints.": "实时协作和写入,不受布局限制。",
|
||||
|
||||
@@ -26,7 +26,6 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) {
|
||||
<>
|
||||
<Head>
|
||||
<title>{t('Docs')}</title>
|
||||
<meta property="og:title" content={t('Docs')} key="title" />
|
||||
<meta
|
||||
name="description"
|
||||
content={t(
|
||||
|
||||
@@ -67,6 +67,14 @@ const DocPage = ({ id }: DocProps) => {
|
||||
useCollaboration(doc?.id, doc?.content);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (doc?.title) {
|
||||
setTimeout(() => {
|
||||
document.title = `${doc.title} - ${t('Docs')}`;
|
||||
}, 100);
|
||||
}
|
||||
}, [doc?.title, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!docQuery || isFetching) {
|
||||
return;
|
||||
@@ -134,21 +142,7 @@ const DocPage = ({ id }: DocProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
{doc.title} - {t('Docs')}
|
||||
</title>
|
||||
<meta
|
||||
property="og:title"
|
||||
content={`${doc.title} - ${t('Docs')}`}
|
||||
key="title"
|
||||
/>
|
||||
</Head>
|
||||
<DocEditor doc={doc} />
|
||||
</>
|
||||
);
|
||||
return <DocEditor doc={doc} />;
|
||||
};
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
/**
|
||||
* @fileOverview This module provides utilities to interact with local storage safely.
|
||||
*/
|
||||
|
||||
interface SyncStorage {
|
||||
getItem(key: string): string | null;
|
||||
setItem(key: string, value: string): void;
|
||||
removeItem(key: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @namespace safeLocalStorage
|
||||
* @description A utility for safely interacting with localStorage.
|
||||
* It checks if the `window` object is defined before attempting to access localStorage,
|
||||
* preventing errors in environments where `window` is not available.
|
||||
*/
|
||||
export const safeLocalStorage: SyncStorage = {
|
||||
/**
|
||||
* Retrieves an item from localStorage.
|
||||
* @param {string} key - The key of the item to retrieve.
|
||||
* @returns {string | null} The item's value, or null if the item does not exist or if localStorage is not available.
|
||||
*/
|
||||
getItem: (key: string): string | null => {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
return localStorage.getItem(key);
|
||||
},
|
||||
/**
|
||||
* Sets an item in localStorage.
|
||||
* @param {string} key - The key of the item to set.
|
||||
* @param {string} value - The value to set for the item.
|
||||
* @returns {void}
|
||||
*/
|
||||
setItem: (key: string, value: string): void => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(key, value);
|
||||
},
|
||||
/**
|
||||
* Removes an item from localStorage.
|
||||
* @param {string} key - The key of the item to remove.
|
||||
* @returns {void}
|
||||
*/
|
||||
removeItem: (key: string): void => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
localStorage.removeItem(key);
|
||||
},
|
||||
};
|
||||
@@ -19,6 +19,7 @@
|
||||
"app:build": "yarn APP_IMPRESS run build",
|
||||
"app:test": "yarn APP_IMPRESS run test",
|
||||
"ci:build": "yarn APP_IMPRESS run build:ci",
|
||||
"build": "yarn APP_IMPRESS run build && yarn COLLABORATION_SERVER run build",
|
||||
"e2e:test": "yarn APP_E2E run test",
|
||||
"lint": "yarn APP_IMPRESS run lint && yarn APP_E2E run lint && yarn workspace eslint-config-impress run lint && yarn I18N run lint && yarn COLLABORATION_SERVER run lint",
|
||||
"i18n:extract": "yarn I18N run extract-translation",
|
||||
@@ -28,11 +29,11 @@
|
||||
"server:test": "yarn COLLABORATION_SERVER run test"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "22.15.29",
|
||||
"@types/react": "19.1.6",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@typescript-eslint/eslint-plugin": "8.33.1",
|
||||
"@typescript-eslint/parser": "8.33.1",
|
||||
"@types/node": "22.15.19",
|
||||
"@types/react": "19.1.4",
|
||||
"@types/react-dom": "19.1.5",
|
||||
"@typescript-eslint/eslint-plugin": "8.32.1",
|
||||
"@typescript-eslint/parser": "8.32.1",
|
||||
"eslint": "8.57.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
|
||||
@@ -6,19 +6,19 @@
|
||||
"lint": "eslint --ext .js ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "15.3.3",
|
||||
"@tanstack/eslint-plugin-query": "5.78.0",
|
||||
"@next/eslint-plugin-next": "15.3.2",
|
||||
"@tanstack/eslint-plugin-query": "5.74.7",
|
||||
"@typescript-eslint/eslint-plugin": "*",
|
||||
"@typescript-eslint/parser": "*",
|
||||
"eslint": "*",
|
||||
"eslint-config-next": "15.3.3",
|
||||
"eslint-config-next": "15.3.2",
|
||||
"eslint-config-prettier": "10.1.5",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-jest": "28.12.0",
|
||||
"eslint-plugin-jest": "28.11.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-playwright": "2.2.0",
|
||||
"eslint-plugin-prettier": "5.4.1",
|
||||
"eslint-plugin-testing-library": "7.4.0",
|
||||
"eslint-plugin-prettier": "5.4.0",
|
||||
"eslint-plugin-testing-library": "7.2.1",
|
||||
"prettier": "3.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import yargs from 'yargs/yargs';
|
||||
|
||||
// Get our args
|
||||
const argv = yargs(process.argv).argv;
|
||||
const argv = yargs(hideBin(process.argv)).argv;
|
||||
const { app, output } = argv;
|
||||
|
||||
const folderPath = './locales/' + app;
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
"jest": "29.7.0",
|
||||
"ts-jest": "29.3.4",
|
||||
"typescript": "*",
|
||||
"yargs": "18.0.0"
|
||||
"yargs": "17.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import yargs from 'yargs/yargs';
|
||||
|
||||
// Get our args
|
||||
const argv = yargs(process.argv).argv;
|
||||
const argv = yargs(hideBin(process.argv)).argv;
|
||||
const { app, output, language } = argv;
|
||||
|
||||
const folderPath = './locales/' + app;
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocknote/server-util": "0.31.1",
|
||||
"@blocknote/server-util": "0.30.1",
|
||||
"@hocuspocus/server": "2.15.2",
|
||||
"@sentry/node": "9.26.0",
|
||||
"@sentry/profiling-node": "9.26.0",
|
||||
"@sentry/node": "9.19.0",
|
||||
"@sentry/profiling-node": "9.19.0",
|
||||
"axios": "1.9.0",
|
||||
"cors": "2.8.5",
|
||||
"express": "5.1.0",
|
||||
|
||||
+338
-1748
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,4 @@
|
||||
{
|
||||
"translations": {
|
||||
"en": {
|
||||
"translation": {
|
||||
"Docs": "MyDocs",
|
||||
"New doc": "+"
|
||||
}
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"default": {
|
||||
"logo": {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"build": "yarn build-mjml-to-html && yarn build-html-to-plain-text"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22"
|
||||
"node": "16.15.1"
|
||||
},
|
||||
"repository": "https://github.com/numerique-gouv/impress",
|
||||
"author": "DINUM",
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# ERB templated nginx configuration
|
||||
# see https://doc.scalingo.com/platform/deployment/buildpacks/nginx
|
||||
|
||||
upstream backend_server {
|
||||
server localhost:8000 fail_timeout=0;
|
||||
}
|
||||
|
||||
upstream collaboration_server {
|
||||
server localhost:4444 fail_timeout=0;
|
||||
}
|
||||
|
||||
server {
|
||||
|
||||
listen <%= ENV["PORT"] %>;
|
||||
server_name _;
|
||||
|
||||
root /app/build/frontend-out;
|
||||
|
||||
error_page 404 /404.html;
|
||||
|
||||
location /collaboration/api/ {
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_redirect off;
|
||||
proxy_pass http://collaboration_server;
|
||||
}
|
||||
|
||||
location /collaboration/ws/ {
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
|
||||
# Set appropriate timeout for WebSocketAdd commentMore actions
|
||||
proxy_read_timeout 86400;
|
||||
proxy_send_timeout 86400;
|
||||
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_redirect off;
|
||||
proxy_pass http://collaboration_server;
|
||||
}
|
||||
|
||||
# Django rest framework
|
||||
location ^~ /api/ {
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_redirect off;
|
||||
proxy_pass http://backend_server;
|
||||
}
|
||||
|
||||
# Django admin
|
||||
location ^~ /admin/ {
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_redirect off;
|
||||
proxy_pass http://backend_server;
|
||||
}
|
||||
|
||||
# Proxy auth for media
|
||||
location /media/ {
|
||||
# Auth request configuration
|
||||
auth_request /media-auth;
|
||||
auth_request_set $authHeader $upstream_http_authorization;
|
||||
auth_request_set $authDate $upstream_http_x_amz_date;
|
||||
auth_request_set $authContentSha256 $upstream_http_x_amz_content_sha256;
|
||||
|
||||
# Pass specific headers from the auth response
|
||||
proxy_set_header Authorization $authHeader;
|
||||
proxy_set_header X-Amz-Date $authDate;
|
||||
proxy_set_header X-Amz-Content-SHA256 $authContentSha256;
|
||||
|
||||
# Get resource from Object Storage
|
||||
proxy_pass <%= ENV["AWS_S3_ENDPOINT_URL"] %>/<%= ENV["AWS_STORAGE_BUCKET_NAME"] %>/;
|
||||
proxy_set_header Host <%= ENV["AWS_S3_ENDPOINT_URL"].split("://")[1] %>;
|
||||
|
||||
add_header Content-Security-Policy "default-src 'none'" always;
|
||||
}
|
||||
|
||||
location /media-auth {
|
||||
proxy_pass http://backend_server/api/v1.0/documents/media-auth/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Original-URL $request_uri;
|
||||
|
||||
# Prevent the body from being passed
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header X-Original-Method $request_method;
|
||||
}
|
||||
|
||||
|
||||
location / {
|
||||
try_files $uri index.html $uri/ =404;
|
||||
}
|
||||
|
||||
location ~ "^/docs/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/?$" {
|
||||
try_files $uri /docs/[id]/index.html;
|
||||
}
|
||||
|
||||
location = /404.html {
|
||||
internal;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user