Compare commits

..

1 Commits

Author SHA1 Message Date
Anthony LC 12d1721e07 🚸(frontend) remove blocking modal on save in Firefox
In firefox the request to save documents did not
complete when the user where closing is tab or browser.
To let the save request complete we added a blocking
modal, but it was not user friendly.
Now we removed the blocking modal and add "keepalive"
option to the fetch request to let the request complete
in background when the user close the tab or browser.
2026-01-11 11:27:21 +01:00
70 changed files with 3686 additions and 3625 deletions
+5 -26
View File
@@ -6,49 +6,28 @@ and this project adheres to
## [Unreleased]
### Added
- ✨(frontend) integrate configurable Waffle #1795
### Fixed
- ✅(e2e) fix e2e test for other browsers #1799
- 🐛(frontend) add fallback for unsupported Blocknote languages #1810
- 🐛(frontend) fix emojipicker closing in tree #1808
### Changed
- ♿(frontend) improve accessibility:
- ♿️(frontend) fix subdoc opening and emoji pick focus #1745
- ♿️(frontend) Keyboard focus Fixes for docs Tree/Editor #1816
## [4.4.0] - 2026-01-13
### Added
- ✨(backend) add documents/all endpoint with descendants #1553
- ✅(export) add PDF regression tests #1762
- 📝(docs) Add language configuration documentation #1757
- 🔒(helm) Set default security context #1750
- ✨(backend) use langfuse to monitor AI actions #1776
- ✨(backend) use langfuse to monitor AI actions
### Changed
- ♿(frontend) improve accessibility:
- ♿(frontend) make html export accessible to screen reader users #1743
- ♿(frontend) add missing label and fix Axes errors to improve a11y #1693
### Fixed
### Fixed
- ✅(backend) reduce flakiness on backend test #1769
- 🐛(frontend) fix clickable main content regression #1773
- 🐛(backend) fix TRASHBIN_CUTOFF_DAYS type error #1778
- 💄(frontend) fix icon position in callout block #1779
- 🚸(frontend) remove blocking modal on save in Firefox #1787
### Security
- 🔒️(backend) validate more strictly url used by cors-proxy endpoint #1768
- 🔒️(frontend) fix props vulnerability in Interlinking #1792
## [4.3.0] - 2026-01-05
@@ -68,6 +47,7 @@ and this project adheres to
- 🐛(frontend) fix tables deletion #1739
- 🐛(frontend) fix children not display when first resize #1753
- 🐛(frontend) fix clickable main content regression #1773
## [4.2.0] - 2025-12-17
@@ -1006,8 +986,7 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/suitenumerique/docs/compare/v4.4.0...main
[v4.4.0]: https://github.com/suitenumerique/docs/releases/v4.4.0
[unreleased]: https://github.com/suitenumerique/docs/compare/v4.3.0...main
[v4.3.0]: https://github.com/suitenumerique/docs/releases/v4.3.0
[v4.2.0]: https://github.com/suitenumerique/docs/releases/v4.2.0
[v4.1.0]: https://github.com/suitenumerique/docs/releases/v4.1.0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

+6 -40
View File
@@ -1,6 +1,4 @@
# Customization Guide 🛠
## Runtime Theming 🎨
# Runtime Theming 🎨
### How to Use
@@ -34,7 +32,7 @@ Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom
----
## Runtime JavaScript Injection 🚀
# Runtime JavaScript Injection 🚀
### How to Use
@@ -89,7 +87,7 @@ Then, set the `FRONTEND_JS_URL` environment variable to the URL of your custom J
----
## **Your Docs icon** 📝
# **Your Docs icon** 📝
You can add your own Docs icon in the header from the theme customization file.
@@ -107,7 +105,7 @@ This configuration is optional. If not set, the default icon will be used.
----
## **Footer Configuration** 📝
# **Footer Configuration** 📝
The footer is configurable from the theme customization file.
@@ -130,7 +128,7 @@ Below is a visual example of a configured footer ⬇️:
----
## **Custom Translations** 📝
# **Custom Translations** 📝
The translations can be partially overridden from the theme customization file.
@@ -142,36 +140,4 @@ 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
----
## **Waffle Configuration** 🧇
The Waffle (La Gaufre) is a widget that displays a grid of services.
![Waffle Configuration Example](./assets/waffle.png)
### Settings 🔧
```shellscript
THEME_CUSTOMIZATION_FILE_PATH=<path>
```
### Configuration
The Waffle can be configured in the theme customization file with the `waffle` key.
### Available Properties
See: [LaGaufreV2Props](https://github.com/suitenumerique/ui-kit/blob/main/src/components/la-gaufre/LaGaufreV2.tsx#L49)
### Complete Example
From the theme customization file: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
### Behavior
- If `data.services` is provided, the Waffle will display those services statically
- If no data is provided, services can be fetched dynamically from an API endpoint thanks to the `apiUrl` property
The json must follow some rules: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
-7
View File
@@ -31,13 +31,6 @@
"matchPackageNames": ["django"],
"allowedVersions": "<6.0.0"
},
{
"groupName": "allowed celery versions",
"matchManagers": ["pep621"],
"matchPackageNames": ["celery"],
"allowedVersions": "<5.6.0"
},
{
"enabled": false,
"groupName": "ignored js dependencies",
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
"PO-Revision-Date: 2026-01-05 08:21\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -79,7 +79,7 @@ msgstr "Doare korf"
msgid "Format"
msgstr "Stumm"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
#, python-brace-format
msgid "copy of {title}"
msgstr "eilenn {title}"
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
"PO-Revision-Date: 2026-01-05 08:21\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -79,7 +79,7 @@ msgstr "Typ"
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
#, python-brace-format
msgid "copy of {title}"
msgstr "Kopie von {title}"
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
"PO-Revision-Date: 2026-01-05 08:21\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -79,7 +79,7 @@ msgstr ""
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
"PO-Revision-Date: 2026-01-05 08:21\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -79,7 +79,7 @@ msgstr "Tipo de Cuerpo"
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
#, python-brace-format
msgid "copy of {title}"
msgstr "copia de {title}"
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
"PO-Revision-Date: 2026-01-05 08:21\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -79,7 +79,7 @@ msgstr "Type de corps"
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
#, python-brace-format
msgid "copy of {title}"
msgstr "copie de {title}"
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
"PO-Revision-Date: 2026-01-05 08:21\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -79,7 +79,7 @@ msgstr ""
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
#, python-brace-format
msgid "copy of {title}"
msgstr "copia di {title}"
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
"PO-Revision-Date: 2026-01-05 08:21\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -79,7 +79,7 @@ msgstr "Text type"
msgid "Format"
msgstr "Formaat"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
#, python-brace-format
msgid "copy of {title}"
msgstr "kopie van {title}"
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
"PO-Revision-Date: 2026-01-05 08:21\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -79,7 +79,7 @@ msgstr "Tipo de corpo"
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
#, python-brace-format
msgid "copy of {title}"
msgstr "cópia de {title}"
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
"PO-Revision-Date: 2026-01-05 08:21\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -79,7 +79,7 @@ msgstr "Тип сообщения"
msgid "Format"
msgstr "Формат"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
#, python-brace-format
msgid "copy of {title}"
msgstr "копия {title}"
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
"PO-Revision-Date: 2026-01-05 08:21\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -79,7 +79,7 @@ msgstr "Vrsta telesa"
msgid "Format"
msgstr "Oblika"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
"PO-Revision-Date: 2026-01-05 08:21\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -79,7 +79,7 @@ msgstr ""
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
"PO-Revision-Date: 2026-01-05 08:21\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -79,7 +79,7 @@ msgstr ""
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
"PO-Revision-Date: 2026-01-05 08:21\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -79,7 +79,7 @@ msgstr "Тип вмісту"
msgid "Format"
msgstr "Формат"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
#, python-brace-format
msgid "copy of {title}"
msgstr "копія {title}"
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2025-12-16 21:44+0000\n"
"PO-Revision-Date: 2026-01-05 08:21\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -79,7 +79,7 @@ msgstr "正文类型"
msgid "Format"
msgstr "格式"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1024 core/api/viewsets.py:1024
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
+2 -2
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "4.4.0"
version = "4.3.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -28,7 +28,7 @@ dependencies = [
"beautifulsoup4==4.14.3",
"boto3==1.42.17",
"Brotli==1.2.0",
"celery[redis]==5.5.3",
"celery[redis]==5.6.0",
"django-configurations==2.5.1",
"django-cors-headers==4.9.0",
"django-countries==8.2.0",
@@ -58,7 +58,7 @@ test.describe('Doc Comments', () => {
await page.getByRole('button', { name: '👍' }).click();
await expect(
thread.getByRole('img', { name: `E2E ${browserName}` }).first(),
thread.getByRole('img', { name: 'E2E Chromium' }).first(),
).toBeVisible();
await expect(thread.getByText('This is a comment').first()).toBeVisible();
await expect(thread.getByText(`E2E ${browserName}`).first()).toBeVisible();
@@ -73,7 +73,7 @@ test.describe('Doc Editor', () => {
await page.keyboard.press('Escape');
await page.locator('.ProseMirror').focus();
await page.locator('.bn-block-outer').last().click();
await page.keyboard.press('Enter');
@@ -21,7 +21,7 @@ test.describe('Inherited share accesses', () => {
`doc-share-member-row-user.test@${browserName}.test`,
);
await expect(user).toBeVisible();
await expect(user.getByText(`E2E ${browserName}`)).toBeVisible();
await expect(user.getByText('E2E Chromium')).toBeVisible();
await expect(user.getByText('Owner')).toBeVisible();
await page
@@ -4,6 +4,7 @@ import {
createDoc,
expectLoginPage,
keyCloakSignIn,
randomName,
updateDocTitle,
verifyDocName,
} from './utils-common';
@@ -19,6 +20,50 @@ test.describe('Doc Tree', () => {
await page.goto('/');
});
test('create new sub pages', async ({ page, browserName }) => {
const [titleParent] = await createDoc(
page,
'doc-tree-content',
browserName,
1,
);
await verifyDocName(page, titleParent);
const addButton = page.getByTestId('new-doc-button');
const docTree = page.getByTestId('doc-tree');
await expect(addButton).toBeVisible();
// Wait for and intercept the POST request to create a new page
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes('/documents/') &&
response.url().includes('/children/') &&
response.request().method() === 'POST',
);
await clickOnAddRootSubPage(page);
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
const subPageJson = await response.json();
await expect(docTree).toBeVisible();
const subPageItem = docTree
.getByTestId(`doc-sub-page-item-${subPageJson.id}`)
.first();
await expect(subPageItem).toBeVisible();
await subPageItem.click();
await verifyDocName(page, '');
const input = page.getByRole('textbox', { name: 'Document title' });
await input.click();
const [randomDocName] = randomName('doc-tree-test', browserName, 1);
await input.fill(randomDocName);
await input.press('Enter');
await expect(subPageItem.getByText(randomDocName)).toBeVisible();
await page.reload();
await expect(subPageItem.getByText(randomDocName)).toBeVisible();
});
test('check the reorder of sub pages', async ({ page, browserName }) => {
await createDoc(page, 'doc-tree-content', browserName, 1);
const addButton = page.getByTestId('new-doc-button');
@@ -59,90 +59,45 @@ test.describe('Header', () => {
).toBeVisible();
await expect(header.getByText('English')).toBeVisible();
await expect(
header.getByRole('button', {
name: 'Les services de La Suite numérique',
}),
).toBeVisible();
});
test('checks a custom waffle', async ({ page }) => {
test('checks La Gauffre interaction', async ({ page }) => {
await overrideConfig(page, {
theme_customization: {
waffle: {
data: {
services: [
{
name: 'Docs E2E Custom 1',
url: 'https://docs.numerique.gouv.fr/',
maturity: 'stable',
logo: 'https://lasuite.numerique.gouv.fr/assets/products/docs.svg',
},
{
name: 'Docs E2E Custom 2',
url: 'https://docs.numerique.gouv.fr/',
maturity: 'stable',
logo: 'https://lasuite.numerique.gouv.fr/assets/products/docs.svg',
},
],
},
showMoreLimit: 9,
},
},
FRONTEND_THEME: 'dsfr',
});
await page.goto('/');
const header = page.locator('header').first();
await expect(
header.getByRole('button', { name: 'Digital LaSuite services' }),
header.getByRole('button', {
name: 'Les services de La Suite numérique',
}),
).toBeVisible();
/**
* The Waffle loads a js file from a remote server,
* it takes some time to load the file and have the interaction available
*/
await page.waitForTimeout(1500);
await header
.getByRole('button', { name: 'Digital LaSuite services' })
.click();
await expect(
page.getByRole('link', { name: 'Docs E2E Custom 1' }),
).toBeVisible();
await expect(
page.getByRole('link', { name: 'Docs E2E Custom 2' }),
).toBeVisible();
});
test('checks the waffle dsfr', async ({ page }) => {
await overrideConfig(page, {
theme_customization: {
waffle: {
apiUrl: 'https://lasuite.numerique.gouv.fr/api/services',
showMoreLimit: 9,
},
},
});
await page.goto('/');
const header = page.locator('header').first();
await expect(
header.getByRole('button', { name: 'Digital LaSuite services' }),
).toBeVisible();
/**
* The Waffle loads a js file from a remote server,
* La gaufre load a js file from a remote server,
* it takes some time to load the file and have the interaction available
*/
await page.waitForTimeout(1500);
await header
.getByRole('button', {
name: 'Digital LaSuite services',
name: 'Les services de La Suite numérique',
})
.click();
await expect(page.getByRole('link', { name: 'Tchap' })).toBeVisible();
await expect(
page.getByRole('link', { name: 'France Transfert' }),
).toBeVisible();
await expect(page.getByRole('link', { name: 'Grist' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Visio' })).toBeVisible();
});
});
@@ -169,6 +124,11 @@ test.describe('Header mobile', () => {
await expect(header.getByLabel('Open the header menu')).toBeVisible();
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
await expect(
header.getByRole('button', {
name: 'Les services de La Suite numérique',
}),
).toBeVisible();
});
});
@@ -113,6 +113,9 @@ test.describe('Home page', () => {
});
await expect(languageButton).toBeVisible();
await expect(
header.getByRole('button', { name: 'Les services de La Suite numé' }),
).toBeVisible();
await expect(
header.getByRole('img', { name: 'Gouvernement Logo' }),
).toBeVisible();
@@ -1,11 +1,6 @@
import { expect, test } from '@playwright/test';
import {
TestLanguage,
createDoc,
overrideConfig,
waitForLanguageSwitch,
} from './utils-common';
import { TestLanguage, createDoc, waitForLanguageSwitch } from './utils-common';
import { openSuggestionMenu } from './utils-editor';
test.describe('Language', () => {
@@ -112,15 +107,6 @@ test.describe('Language', () => {
page,
browserName,
}) => {
await overrideConfig(page, {
LANGUAGES: [
['en-us', 'English'],
['fr-fr', 'Français'],
['sv-se', 'Svenska'],
],
LANGUAGE_CODE: 'en-us',
});
await createDoc(page, 'doc-toolbar', browserName, 1);
const { editor, suggestionMenu } = await openSuggestionMenu({ page });
@@ -140,14 +126,5 @@ test.describe('Language', () => {
await expect(
suggestionMenu.getByText('Titres', { exact: true }),
).toBeVisible();
/**
* Swedish is not yet supported in the BlockNote locales, so it should fallback to English
*/
await waitForLanguageSwitch(page, TestLanguage.Swedish);
await openSuggestionMenu({ page });
await expect(
suggestionMenu.getByText('Headings', { exact: true }),
).toBeVisible();
});
});
@@ -224,9 +224,7 @@ export const updateDocTitle = async (page: Page, title: string) => {
await expect(input).toHaveText('');
await expect(input).toBeVisible();
await input.click();
await input.fill(title, {
force: true,
});
await input.fill(title);
await input.click();
await input.blur();
await verifyDocName(page, title);
@@ -330,10 +328,6 @@ export const TestLanguage = {
label: 'Deutsch',
expectedLocale: ['de-de'],
},
Swedish: {
label: 'Svenska',
expectedLocale: ['sv-se'],
},
} as const;
type TestLanguageKey = keyof typeof TestLanguage;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "4.4.0",
"version": "4.3.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
+356 -49
View File
@@ -1,53 +1,58 @@
import {
dsfrGlobals,
getUIKitThemesFromGlobals,
whiteLabelGlobals,
} from '@gouvfr-lasuite/ui-kit';
import { defaultTokens } from '@gouvfr-lasuite/cunningham-react';
import { cunninghamConfig as tokens } from '@gouvfr-lasuite/ui-kit';
import merge from 'lodash/merge';
const themeWhiteLabelLight = getUIKitThemesFromGlobals(whiteLabelGlobals, {
prefix: 'default',
variants: ['light'],
overrides: {
globals: {
spacing: {
'0': '0rem',
none: '0rem',
auto: 'auto',
bx: '2.2rem',
full: '100%',
'3xs': '0.25rem',
'2xs': '0.375rem',
},
},
components: {
logo: {
src: '',
alt: '',
widthHeader: '',
widthFooter: '',
},
'home-proconnect': false,
icon: {
src: '/assets/icon-docs.svg',
width: '32px',
height: 'auto',
},
favicon: {
'png-light': '/assets/favicon-light.png',
'png-dark': '/assets/favicon-dark.png',
// Uikit does not provide the full list of tokens.
// To be able to override correctly, we need to merge with the default tokens.
let mergedColors = merge(
defaultTokens.globals.colors,
tokens.themes.default.globals.colors,
);
mergedColors = {
...mergedColors,
'logo-1': '#2845C1',
};
tokens.themes.default.globals = {
...tokens.themes.default.globals,
...{
colors: mergedColors,
font: {
...tokens.themes.default.globals.font,
families: {
base: 'sans-serif',
accent: 'sans-serif',
},
},
},
});
const themeDefault = {
default: themeWhiteLabelLight['default-light'],
};
const themesDSFRLight = getUIKitThemesFromGlobals(dsfrGlobals, {
prefix: 'dsfr',
variants: ['light'],
overrides: {
tokens.themes.default.components = {
...tokens.themes.default.components,
...{
logo: {
src: '',
alt: '',
widthHeader: '',
widthFooter: '',
},
'la-gaufre': false,
'home-proconnect': false,
icon: {
src: '/assets/icon-docs.svg',
width: '32px',
height: 'auto',
},
favicon: {
'png-light': '/assets/favicon-light.png',
'png-dark': '/assets/favicon-dark.png',
},
},
};
const dsfrTheme = {
dsfr: {
globals: {
font: {
families: {
@@ -63,6 +68,7 @@ const themesDSFRLight = getUIKitThemesFromGlobals(dsfrGlobals, {
widthFooter: '220px',
alt: 'Gouvernement Logo',
},
'la-gaufre': true,
'home-proconnect': true,
icon: {
src: '/assets/icon-docs-dsfr.svg',
@@ -76,16 +82,317 @@ const themesDSFRLight = getUIKitThemesFromGlobals(dsfrGlobals, {
},
},
},
});
};
const themesDSFR = {
dsfr: themesDSFRLight['dsfr-light'],
const genericTheme = {
generic: {
globals: {
colors: {
'brand-050': '#EEF1FA',
'brand-100': '#DDE2F5',
'brand-150': '#CED3F1',
'brand-200': '#BEC5F0',
'brand-250': '#AFB5F1',
'brand-300': '#A0A5F6',
'brand-350': '#8F94FD',
'brand-400': '#8184FC',
'brand-450': '#7576EE',
'brand-500': '#6969DF',
'brand-550': '#5E5CD0',
'brand-600': '#534FC2',
'brand-650': '#4844AD',
'brand-700': '#3E3B98',
'brand-750': '#36347D',
'brand-800': '#2D2F5F',
'brand-850': '#262848',
'brand-900': '#1C1E32',
'brand-950': '#11131F',
'gray-000': '#FFFFFF',
'gray-025': '#F8F8F9',
'gray-050': '#F0F0F3',
'gray-100': '#E2E2EA',
'gray-150': '#D3D4E0',
'gray-200': '#C5C6D5',
'gray-250': '#B7B7CB',
'gray-300': '#A9A9BF',
'gray-350': '#9C9CB2',
'gray-400': '#8F8FA4',
'gray-450': '#828297',
'gray-500': '#75758A',
'gray-550': '#69697D',
'gray-600': '#5D5D70',
'gray-650': '#515164',
'gray-700': '#454558',
'gray-750': '#3A3A4C',
'gray-800': '#2F303D',
'gray-850': '#25252F',
'gray-900': '#1B1B23',
'gray-950': '#111114',
'gray-1000': '#000000',
'info-050': '#EAF2F9',
'info-100': '#D5E4F3',
'info-150': '#BFD7F0',
'info-200': '#A7CAEE',
'info-250': '#8DBDEF',
'info-300': '#6EB0F2',
'info-350': '#50A2F5',
'info-400': '#3593F4',
'info-450': '#1185ED',
'info-500': '#0077DE',
'info-550': '#0069CF',
'info-600': '#005BC0',
'info-650': '#0D4EAA',
'info-700': '#124394',
'info-750': '#163878',
'info-800': '#192F5A',
'info-850': '#192541',
'info-900': '#141B2D',
'info-950': '#0C111C',
'success-050': '#E8F1EA',
'success-100': '#CFE4D4',
'success-150': '#BAD9C1',
'success-200': '#A2CFAD',
'success-250': '#86C597',
'success-300': '#6CBA83',
'success-350': '#4FB070',
'success-400': '#40A363',
'success-450': '#309556',
'success-500': '#1E884A',
'success-550': '#027B3E',
'success-600': '#016D31',
'success-650': '#006024',
'success-700': '#005317',
'success-750': '#0D4511',
'success-800': '#11380E',
'success-850': '#132A11',
'success-900': '#101E0F',
'success-950': '#091209',
'warning-050': '#F8F0E9',
'warning-100': '#F1E0D3',
'warning-150': '#ECD0BC',
'warning-200': '#E8C0A4',
'warning-250': '#E8AE8A',
'warning-300': '#EB9970',
'warning-350': '#E98456',
'warning-400': '#E57036',
'warning-450': '#DA5E18',
'warning-500': '#CB5000',
'warning-550': '#BC4200',
'warning-600': '#AD3300',
'warning-650': '#9E2300',
'warning-700': '#882011',
'warning-750': '#731E16',
'warning-800': '#58201A',
'warning-850': '#401D18',
'warning-900': '#2E1714',
'warning-950': '#1D0F0D',
'error-050': '#F9EFEC',
'error-100': '#F4DFD9',
'error-150': '#F0CEC6',
'error-200': '#EEBCB2',
'error-250': '#EEA99D',
'error-300': '#EF9486',
'error-350': '#F37C6E',
'error-400': '#F65F53',
'error-450': '#F0463D',
'error-500': '#E82322',
'error-550': '#D7010E',
'error-600': '#C00100',
'error-650': '#AA0000',
'error-700': '#910C06',
'error-750': '#731E16',
'error-800': '#58201A',
'error-850': '#401D18',
'error-900': '#2E1714',
'error-950': '#1D0F0D',
'red-050': '#FAEFEE',
'red-100': '#F4DEDD',
'red-150': '#F1CDCB',
'red-200': '#EFBBBA',
'red-250': '#EEA8A8',
'red-300': '#F09394',
'red-350': '#F37B7E',
'red-400': '#EF6569',
'red-450': '#E94A55',
'red-500': '#DA3B49',
'red-550': '#CA2A3C',
'red-600': '#BB1330',
'red-650': '#A90021',
'red-700': '#910A13',
'red-750': '#731E16',
'red-800': '#58201A',
'red-850': '#411D18',
'red-900': '#2E1714',
'red-950': '#1D0F0D',
'orange-050': '#F8F0E9',
'orange-100': '#F1E0D3',
'orange-150': '#ECD0BD',
'orange-200': '#EABFA6',
'orange-250': '#EBAC90',
'orange-300': '#EC9772',
'orange-350': '#E5845A',
'orange-400': '#D6774D',
'orange-450': '#C86A40',
'orange-500': '#B95D33',
'orange-550': '#AB5025',
'orange-600': '#9D4315',
'orange-650': '#8F3600',
'orange-700': '#812900',
'orange-750': '#6C2511',
'orange-800': '#572017',
'orange-850': '#401D18',
'orange-900': '#2E1714',
'orange-950': '#1D0F0D',
'brown-050': '#F6F0E8',
'brown-100': '#F1E0D3',
'brown-150': '#EBD0BA',
'brown-200': '#E2C0A6',
'brown-250': '#D4B398',
'brown-300': '#C6A58B',
'brown-350': '#B8987E',
'brown-400': '#AA8B71',
'brown-450': '#9D7E65',
'brown-500': '#8F7158',
'brown-550': '#82654C',
'brown-600': '#765841',
'brown-650': '#694C35',
'brown-700': '#5D412A',
'brown-750': '#51361E',
'brown-800': '#452A13',
'brown-850': '#392008',
'brown-900': '#29180A',
'brown-950': '#1B0F08',
'yellow-050': '#F3F0E7',
'yellow-100': '#E9E2CF',
'yellow-150': '#E1D4B7',
'yellow-200': '#D9C599',
'yellow-250': '#D2B677',
'yellow-300': '#CAA756',
'yellow-350': '#C2972E',
'yellow-400': '#B98900',
'yellow-450': '#AB7B00',
'yellow-500': '#9D6E00',
'yellow-550': '#916100',
'yellow-600': '#855400',
'yellow-650': '#784700',
'yellow-700': '#6C3A00',
'yellow-750': '#5F2E00',
'yellow-800': '#512302',
'yellow-850': '#3E1D10',
'yellow-900': '#2D1711',
'yellow-950': '#1D0F0D',
'green-050': '#E6F1E9',
'green-100': '#CFE4D5',
'green-150': '#B8D8C1',
'green-200': '#A0CFAE',
'green-250': '#84C59A',
'green-300': '#65BA86',
'green-350': '#45B173',
'green-400': '#23A562',
'green-450': '#029755',
'green-500': '#008948',
'green-550': '#017B3B',
'green-600': '#006E2E',
'green-650': '#006022',
'green-700': '#005314',
'green-750': '#0D4510',
'green-800': '#11380E',
'green-850': '#132A11',
'green-900': '#101E0F',
'green-950': '#091209',
'blue1-050': '#EBF1F9',
'blue1-100': '#D6E4F4',
'blue1-150': '#C1D7F0',
'blue1-200': '#AACAEF',
'blue1-250': '#8FBCEF',
'blue1-300': '#7CAFEB',
'blue1-350': '#68A1E4',
'blue1-400': '#5B94D6',
'blue1-450': '#4E86C7',
'blue1-500': '#4279B9',
'blue1-550': '#356CAC',
'blue1-600': '#28609E',
'blue1-650': '#1B5390',
'blue1-700': '#0B4783',
'blue1-750': '#0F3C6E',
'blue1-800': '#133059',
'blue1-850': '#152641',
'blue1-900': '#121C2D',
'blue1-950': '#0B111C',
'blue2-050': '#E7F3F4',
'blue2-100': '#CEE7E9',
'blue2-150': '#B2DCE0',
'blue2-200': '#91D1D7',
'blue2-250': '#68C7D0',
'blue2-300': '#43BBC5',
'blue2-350': '#00AFBA',
'blue2-400': '#01A0AA',
'blue2-450': '#00929D',
'blue2-500': '#00848F',
'blue2-550': '#007682',
'blue2-600': '#016874',
'blue2-650': '#005B67',
'blue2-700': '#004E5A',
'blue2-750': '#00424E',
'blue2-800': '#003642',
'blue2-850': '#002A38',
'blue2-900': '#061E28',
'blue2-950': '#071219',
'purple-050': '#F7F0F6',
'purple-100': '#EEE0EE',
'purple-150': '#E7D1E7',
'purple-200': '#DBBFE4',
'purple-250': '#D3AEE2',
'purple-300': '#CB99E1',
'purple-350': '#C188D9',
'purple-400': '#B47BCB',
'purple-450': '#A66EBD',
'purple-500': '#9961AF',
'purple-550': '#8B55A1',
'purple-600': '#7E4894',
'purple-650': '#723C87',
'purple-700': '#633376',
'purple-750': '#552A65',
'purple-800': '#452551',
'purple-850': '#35213D',
'purple-900': '#261A2C',
'purple-950': '#17111C',
'pink-050': '#F8EFF4',
'pink-100': '#F0DFEA',
'pink-150': '#EACEDF',
'pink-200': '#E9BBD1',
'pink-250': '#E9A7C2',
'pink-300': '#E095B4',
'pink-350': '#D685A8',
'pink-400': '#C7799B',
'pink-450': '#B86C8D',
'pink-500': '#AA5F80',
'pink-550': '#9C5374',
'pink-600': '#8E4767',
'pink-650': '#813B5B',
'pink-700': '#732E4F',
'pink-750': '#632643',
'pink-800': '#521F38',
'pink-850': '#3E1C2B',
'pink-900': '#2D171F',
'pink-950': '#1C0E12',
},
font: {
families: {
base: 'Inter, Roboto Flex Variable, sans-serif',
accent: 'Inter, Roboto Flex Variable, sans-serif',
},
},
},
},
};
const docsTokens = {
...tokens,
themes: {
...themeDefault,
...themesDSFR,
...tokens.themes,
...dsfrTheme,
...genericTheme,
},
};
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "4.4.0",
"version": "4.3.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -68,7 +68,6 @@
"react-select": "5.10.2",
"styled-components": "6.1.19",
"use-debounce": "10.0.6",
"uuid": "13.0.0",
"y-protocols": "1.0.7",
"yjs": "*",
"zustand": "5.0.9"
@@ -3,8 +3,6 @@ export * from './BoxButton';
export * from './Card';
export * from './DropButton';
export * from './dropdown-menu/DropdownMenu';
export * from './Emoji/EmojiPicker';
export { default as emojidata } from './Emoji/initEmojiCallout';
export * from './quick-search';
export * from './Icon';
export * from './InfiniteScroll';
@@ -4,14 +4,13 @@ import { Resource } from 'i18next';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { Theme } from '@/cunningham/';
import { FooterType } from '@/features/footer';
import { HeaderType, WaffleType } from '@/features/header';
import { HeaderType } from '@/features/header';
import { PostHogConf } from '@/services';
interface ThemeCustomization {
footer?: FooterType;
translations?: Resource;
header?: HeaderType;
waffle?: WaffleType;
}
export interface ConfigResponse {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -28,7 +28,7 @@ const getMergedTokens = (theme: Theme) => {
return merge({}, tokens.themes['default'], tokens.themes[theme]);
};
const DEFAULT_THEME: Theme = 'default';
const DEFAULT_THEME: Theme = 'generic';
const defaultTokens = getMergedTokens(DEFAULT_THEME);
const initialState: ThemeStore = {
@@ -93,10 +93,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
useSaveDoc(doc.id, provider.document, isConnectedToCollabServer);
const { i18n } = useTranslation();
let lang = i18n.resolvedLanguage;
if (!lang || !(lang in locales)) {
lang = 'en';
}
const lang = i18n.resolvedLanguage;
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
@@ -262,6 +259,7 @@ export const BlockNoteReader = ({
const { user } = useAuth();
const { setEditor } = useEditorStore();
const { threadStore } = useComments(docId, false, user);
const { t } = useTranslation();
const editor = useCreateBlockNote(
{
collaboration: {
@@ -307,6 +305,7 @@ export const BlockNoteReader = ({
editor={editor}
editable={false}
theme="light"
aria-label={t('Document viewer')}
formattingToolbar={false}
slashMenu={false}
comments={false}
@@ -5,7 +5,6 @@ import { Box, Loading } from '@/components';
import { DocHeader } from '@/docs/doc-header/';
import {
Doc,
useDocFocusManagement,
useIsCollaborativeEditable,
useProviderStore,
} from '@/docs/doc-management';
@@ -83,9 +82,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
const readOnly =
!doc.abilities.partial_update || !isEditable || isLoading || isDeletedDoc;
const { setIsSkeletonVisible } = useSkeletonStore();
const isProviderReady = Boolean(isReady && provider);
useDocFocusManagement(doc.id, isProviderReady);
const isProviderReady = isReady && provider;
useEffect(() => {
if (isProviderReady) {
@@ -5,14 +5,11 @@ import { useTranslation } from 'react-i18next';
import { Box } from '@/components';
export const PICKER_HEIGHT = 500;
interface EmojiPickerProps {
emojiData: EmojiMartData;
onClickOutside: () => void;
onEmojiSelect: ({ native }: { native: string }) => void;
withOverlay?: boolean;
onEscape?: () => void;
}
export const EmojiPicker = ({
@@ -20,26 +17,14 @@ export const EmojiPicker = ({
onClickOutside,
onEmojiSelect,
withOverlay = false,
onEscape,
}: EmojiPickerProps) => {
const { i18n } = useTranslation();
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Escape') {
if (onEscape) {
onEscape();
} else {
onClickOutside();
}
}
};
const pickerContent = (
<Box $position="absolute" $zIndex={1000} onKeyDownCapture={handleKeyDown}>
<Box $position="absolute" $zIndex={1000} $margin="2rem 0 0 0">
<Picker
data={emojiData}
locale={i18n.resolvedLanguage}
autoFocus
onClickOutside={onClickOutside}
onEmojiSelect={onEmojiSelect}
previewPosition="none"
@@ -12,9 +12,12 @@ import { TFunction } from 'i18next';
import React, { useEffect, useState } from 'react';
import { createGlobalStyle, css } from 'styled-components';
import { Box, BoxButton, EmojiPicker, Icon, emojidata } from '@/components';
import { Box, BoxButton, Icon } from '@/components';
import { DocsBlockNoteEditor } from '../../types';
import { EmojiPicker } from '../EmojiPicker';
import emojidata from './initEmojiCallout';
const CalloutBlockStyle = createGlobalStyle`
.bn-block-content[data-content-type="callout"][data-background-color] {
@@ -94,15 +97,15 @@ const CalloutComponent = ({
`}
>
<CalloutBlockStyle />
<Box
$position="relative"
$css={css`
align-self: start;
`}
>
<Box $position="relative">
<BoxButton
contentEditable={false}
onClick={toggleEmojiPicker}
onKeyDown={(e) => {
if (e.key === 'Escape' && openEmojiPicker) {
setOpenEmojiPicker(false);
}
}}
$css={css`
font-size: 1.125rem;
cursor: ${isEditable ? 'pointer' : 'default'};
@@ -1,4 +1,5 @@
export * from './AccessibleImageBlock';
export * from './CalloutBlock';
export { default as emojidata } from './initEmojiCallout';
export * from './PdfBlock';
export * from './UploadLoaderBlock';
@@ -1,23 +1,21 @@
import {
PartialCustomInlineContentFromConfig,
StyleSchema,
} from '@blocknote/core';
/* eslint-disable react-hooks/rules-of-hooks */
import { createReactInlineContentSpec } from '@blocknote/react';
import * as Sentry from '@sentry/nextjs';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { css } from 'styled-components';
import { validate as uuidValidate } from 'uuid';
import { BoxButton, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import SelectedPageIcon from '@/docs/doc-editor/assets/doc-selected.svg';
import { getEmojiAndTitle, useDoc } from '@/docs/doc-management/';
import { getEmojiAndTitle, useDoc } from '@/docs/doc-management';
export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
{
type: 'interlinkingLinkInline',
propSchema: {
url: {
default: '',
},
docId: {
default: '',
},
@@ -29,97 +27,46 @@ export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
},
{
render: ({ editor, inlineContent, updateInlineContent }) => {
if (!inlineContent.props.docId) {
return null;
}
const { data: doc } = useDoc({ id: inlineContent.props.docId });
const isEditable = editor.isEditable;
/**
* Should not happen
* Update the content title if the referenced doc title changes
*/
if (!uuidValidate(inlineContent.props.docId)) {
Sentry.captureException(
new Error(`Invalid docId: ${inlineContent.props.docId}`),
{
extra: { info: 'InterlinkingLinkInlineContent' },
},
);
useEffect(() => {
if (
isEditable &&
doc?.title &&
doc.title !== inlineContent.props.title
) {
updateInlineContent({
type: 'interlinkingLinkInline',
props: {
...inlineContent.props,
title: doc.title,
},
});
}
updateInlineContent({
type: 'interlinkingLinkInline',
props: {
docId: '',
title: '',
},
});
/**
* When doing collaborative editing, doc?.title might be out of sync
* causing an infinite loop of updates.
* To prevent this, we only run this effect when doc?.title changes,
* not when inlineContent.props.title changes.
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [doc?.title, isEditable]);
return null;
}
return (
<LinkSelected
docId={inlineContent.props.docId}
title={inlineContent.props.title}
isEditable={editor.isEditable}
updateInlineContent={updateInlineContent}
/>
);
return <LinkSelected {...inlineContent.props} />;
},
},
);
interface LinkSelectedProps {
docId: string;
url: string;
title: string;
isEditable: boolean;
updateInlineContent: (
update: PartialCustomInlineContentFromConfig<
{
readonly type: 'interlinkingLinkInline';
readonly propSchema: {
readonly docId: {
readonly default: '';
};
readonly title: {
readonly default: '';
};
};
readonly content: 'none';
},
StyleSchema
>,
) => void;
}
export const LinkSelected = ({
docId,
title,
isEditable,
updateInlineContent,
}: LinkSelectedProps) => {
const { data: doc } = useDoc({ id: docId });
/**
* Update the content title if the referenced doc title changes
*/
useEffect(() => {
if (isEditable && doc?.title && doc.title !== title) {
updateInlineContent({
type: 'interlinkingLinkInline',
props: {
docId,
title: doc.title,
},
});
}
/**
* When doing collaborative editing, doc?.title might be out of sync
* causing an infinite loop of updates.
* To prevent this, we only run this effect when doc?.title changes,
* not when inlineContent.props.title changes.
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [doc?.title, docId, isEditable]);
const LinkSelected = ({ url, title }: LinkSelectedProps) => {
const { colorsTokens } = useCunninghamTheme();
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(title);
@@ -127,7 +74,7 @@ export const LinkSelected = ({
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
void router.push(`/docs/${docId}/`);
void router.push(url);
};
return (
@@ -247,6 +247,7 @@ export const SearchPage = ({
{
type: 'interlinkingLinkInline',
props: {
url: `/docs/${doc.id}`,
docId: doc.id,
title: doc.title || untitledDocument,
},
@@ -1,3 +1,4 @@
export * from './BlockNoteEditor';
export * from './DocEditor';
export * from './EmojiPicker';
export * from './custom-blocks/';
@@ -4,7 +4,6 @@ import * as Y from 'yjs';
import { useUpdateDoc } from '@/docs/doc-management/';
import { KEY_LIST_DOC_VERSIONS } from '@/docs/doc-versioning';
import { isFirefox } from '@/utils/userAgent';
import { toBase64 } from '../utils';
@@ -62,24 +61,8 @@ export const useSaveDoc = (
const router = useRouter();
useEffect(() => {
const onSave = (e?: Event) => {
const isSaving = saveDoc();
/**
* Firefox does not trigger the request every time the user leaves the page.
* Plus the request is not intercepted by the service worker.
* So we prevent the default behavior to have the popup asking the user
* if he wants to leave the page, by adding the popup, we let the time to the
* request to be sent, and intercepted by the service worker (for the offline part).
*/
if (
isSaving &&
typeof e !== 'undefined' &&
e.preventDefault &&
isFirefox()
) {
e.preventDefault();
}
const onSave = () => {
saveDoc();
};
// Save every minute
@@ -1,24 +1,16 @@
import { ExternalHyperlink, TextRun } from 'docx';
import { getEmojiAndTitle } from '@/docs/doc-management';
import { DocsExporterDocx } from '../types';
export const inlineContentMappingInterlinkingLinkDocx: DocsExporterDocx['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
(inline) => {
if (!inline.props.docId) {
return new TextRun('');
}
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(inline.props.title);
return new ExternalHyperlink({
children: [
new TextRun({
text: `${emoji || '📄'}${titleWithoutEmoji}`,
text: `📄${inline.props.title}`,
bold: true,
}),
],
link: window.location.origin + `/docs/${inline.props.docId}/`,
link: window.location.origin + inline.props.url,
});
};
@@ -1,17 +1,11 @@
import React from 'react';
import { getEmojiAndTitle } from '@/docs/doc-management';
import { DocsExporterODT } from '../types';
export const inlineContentMappingInterlinkingLinkODT: DocsExporterODT['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
(inline) => {
if (!inline.props.docId) {
return null;
}
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(inline.props.title);
const url = window.location.origin + `/docs/${inline.props.docId}/`;
const url = window.location.origin + inline.props.url;
const title = inline.props.title;
// Create ODT hyperlink using React.createElement to avoid TypeScript JSX namespace issues
// Uses the same structure as BlockNote's default link mapping
@@ -24,6 +18,6 @@ export const inlineContentMappingInterlinkingLinkODT: DocsExporterODT['mappings'
xlinkShow: 'replace',
xlinkHref: url,
},
`${emoji || '📄'}${titleWithoutEmoji}`,
`📄${title}`,
);
};
@@ -1,29 +1,21 @@
import { Image, Link, Text } from '@react-pdf/renderer';
import { getEmojiAndTitle } from '@/docs/doc-management';
import DocSelectedIcon from '../assets/doc-selected.png';
import { DocsExporterPDF } from '../types';
export const inlineContentMappingInterlinkingLinkPDF: DocsExporterPDF['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
(inline) => {
if (!inline.props.docId) {
return <></>;
}
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(inline.props.title);
return (
<Link
src={window.location.origin + `/docs/${inline.props.docId}/`}
src={window.location.origin + inline.props.url}
style={{
textDecoration: 'none',
color: 'black',
}}
>
{' '}
{emoji || <Image src={DocSelectedIcon.src} />}{' '}
<Text>{titleWithoutEmoji}</Text>{' '}
<Image src={DocSelectedIcon.src} />{' '}
<Text>{inline.props.title}</Text>{' '}
</Link>
);
};
@@ -24,6 +24,7 @@ export const updateDoc = async ({
body: JSON.stringify({
...params,
}),
keepalive: true,
});
if (!response.ok) {
@@ -1,27 +1,10 @@
import { MouseEvent, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import {
Box,
BoxButton,
BoxButtonType,
EmojiPicker,
PICKER_HEIGHT,
Text,
TextType,
emojidata,
} from '@/components';
import { BoxButton, BoxButtonType, Text, TextType } from '@/components';
import { EmojiPicker, emojidata } from '@/docs/doc-editor/';
import { useDocTitleUpdate } from '../hooks/useDocTitleUpdate';
import { cssSelectors } from '../utils';
const getClosestTreeItem = (element: HTMLElement | null) =>
element?.closest<HTMLElement>(cssSelectors.DOC_TREE_ROW) ??
element?.closest<HTMLElement>(cssSelectors.DOC_TREE_NODE) ??
element?.closest<HTMLElement>('[role="treeitem"]') ??
null;
type DocIconProps = TextType & {
buttonProps?: BoxButtonType;
@@ -47,7 +30,6 @@ export const DocIcon = ({
...textProps
}: DocIconProps) => {
const { updateDocEmoji } = useDocTitleUpdate();
const { t } = useTranslation();
const iconRef = useRef<HTMLDivElement>(null);
@@ -61,14 +43,6 @@ export const DocIcon = ({
return defaultIcon;
}
const emojiLabel = withEmojiPicker
? emoji
? t('Edit document emoji')
: t('Add emoji')
: emoji
? t('Document emoji')
: undefined;
const toggleEmojiPicker = (e: MouseEvent) => {
if (withEmojiPicker) {
e.stopPropagation();
@@ -76,24 +50,9 @@ export const DocIcon = ({
if (!openEmojiPicker && iconRef.current) {
const rect = iconRef.current.getBoundingClientRect();
const pickerHeight = PICKER_HEIGHT;
const spaceBelow = window.innerHeight - rect.bottom;
const spaceAbove = rect.top;
// Position picker above if not enough space below and enough space above
const shouldPositionAbove =
spaceBelow < pickerHeight && spaceAbove >= pickerHeight;
// Offset to align the picker properly
const ROW_OFFSET_TOP = 55;
const ROW_OFFSET_BOTTOM = 10;
setPickerPosition({
top: shouldPositionAbove
? rect.top - pickerHeight + ROW_OFFSET_TOP
: rect.bottom + ROW_OFFSET_BOTTOM,
left: rect.left,
top: rect.bottom + window.scrollY + 8,
left: rect.left + window.scrollX,
});
}
@@ -117,30 +76,6 @@ export const DocIcon = ({
setOpenEmojiPicker(false);
};
const handleEscape = () => {
setOpenEmojiPicker(false);
window.requestAnimationFrame(() => {
const localTreeItem = getClosestTreeItem(iconRef.current);
const docTree = document.querySelector<HTMLElement>(
cssSelectors.DOC_TREE,
);
const docTreeItem =
localTreeItem ||
docTree?.querySelector<HTMLElement>(
cssSelectors.DOC_TREE_FOCUSED_NODE,
) ||
docTree?.querySelector<HTMLElement>(
cssSelectors.DOC_TREE_SELECTED_ROW,
) ||
docTree?.querySelector<HTMLElement>(
cssSelectors.DOC_TREE_SELECTED_NODE,
) ||
document.querySelector<HTMLElement>(cssSelectors.DOC_TREE_ROOT);
docTreeItem?.focus();
});
};
return (
<>
<BoxButton
@@ -148,8 +83,6 @@ export const DocIcon = ({
ref={iconRef}
onClick={toggleEmojiPicker}
color="tertiary-text"
aria-label={emojiLabel}
title={emojiLabel}
{...buttonProps}
>
{!emoji ? (
@@ -169,22 +102,21 @@ export const DocIcon = ({
</BoxButton>
{openEmojiPicker &&
createPortal(
<Box
$position="fixed"
$css={css`
top: ${pickerPosition.top}px;
left: ${pickerPosition.left}px;
z-index: 1000;
`}
<div
style={{
position: 'absolute',
top: pickerPosition.top,
left: pickerPosition.left,
zIndex: 1000,
}}
>
<EmojiPicker
emojiData={emojidata}
onEmojiSelect={handleEmojiSelect}
onClickOutside={handleClickOutside}
withOverlay={true}
onEscape={handleEscape}
/>
</Box>,
</div>,
document.body,
)}
</>
@@ -1,7 +1,6 @@
export * from './useCollaboration';
export * from './useCopyDocLink';
export * from './useCreateChildDocTree';
export * from './useDocFocusManagement';
export * from './useDocTitleUpdate';
export * from './useDocUtils';
export * from './useIsCollaborativeEditable';
@@ -1,91 +0,0 @@
import { useEffect } from 'react';
import { cssSelectors } from '@/docs/doc-management/utils';
const isWithin = (el: Element | null, selector: string) =>
!!el?.closest(selector);
export const useDocFocusManagement = (docId?: string, isReady = true) => {
// 1) Auto-focus title when opening a doc
useEffect(() => {
if (!docId || !isReady || typeof window === 'undefined') {
return;
}
const frameId = window.requestAnimationFrame(() => {
const titleElement = document.querySelector<HTMLElement>(
cssSelectors.DOC_TITLE,
);
if (!titleElement) {
return;
}
// Avoid stealing focus if user is already in the doc tree or editor.
const activeEl = document.activeElement;
const active = activeEl instanceof Element ? activeEl : null;
const isInDocUI =
isWithin(active, cssSelectors.DOC_EDITOR_FOCUS) ||
isWithin(active, cssSelectors.DOC_TREE);
const isBodyFocused = activeEl === document.body;
if (isBodyFocused && !isInDocUI && activeEl !== titleElement) {
titleElement.focus();
}
});
return () => window.cancelAnimationFrame(frameId);
}, [docId, isReady]);
// 2) Escape from editor/title -> focus back the selected tree item (or root)
useEffect(() => {
if (!docId || !isReady || typeof window === 'undefined') {
return;
}
const handleFocusShortcut = (event: KeyboardEvent) => {
if (event.key !== 'F6' || event.defaultPrevented) {
return;
}
const target = event.target instanceof Element ? event.target : null;
const activeEl = document.activeElement;
const active = activeEl instanceof Element ? activeEl : null;
const isDocFocus =
isWithin(target, cssSelectors.DOC_EDITOR_FOCUS) ||
isWithin(active, cssSelectors.DOC_EDITOR_FOCUS) ||
isWithin(target, cssSelectors.DOC_TITLE) ||
isWithin(active, cssSelectors.DOC_TITLE);
if (!isDocFocus) {
return;
}
const docTree = document.querySelector<HTMLElement>(
cssSelectors.DOC_TREE,
);
const docTreeItem =
docTree?.querySelector<HTMLElement>(
cssSelectors.DOC_TREE_SELECTED_ROW,
) ||
docTree?.querySelector<HTMLElement>(
cssSelectors.DOC_TREE_SELECTED_NODE,
) ||
document.querySelector<HTMLElement>(cssSelectors.DOC_TREE_ROOT);
if (!docTreeItem) {
return;
}
docTreeItem.focus();
event.preventDefault();
event.stopPropagation();
};
document.addEventListener('keydown', handleFocusShortcut, true);
return () =>
document.removeEventListener('keydown', handleFocusShortcut, true);
}, [docId, isReady]);
};
@@ -38,15 +38,3 @@ export const getEmojiAndTitle = (title: string) => {
return { emoji: null, titleWithoutEmoji: title };
};
export const cssSelectors = {
DOC_TITLE: '.--docs--doc-title-input[contenteditable="true"]',
DOC_TREE_ROOT: '[data-testid="doc-tree-root-item"]',
DOC_TREE: '[data-testid="doc-tree"]',
DOC_EDITOR_FOCUS: '.--docs--main-editor, .--docs--doc-title-input',
DOC_TREE_ROW: '.c__tree-view--row',
DOC_TREE_NODE: '.c__tree-view--node',
DOC_TREE_FOCUSED_NODE: '.c__tree-view--node.isFocused',
DOC_TREE_SELECTED_ROW: '.c__tree-view--row[aria-selected="true"]',
DOC_TREE_SELECTED_NODE: '.c__tree-view--node[aria-selected="true"]',
} as const;
@@ -199,6 +199,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
<ButtonCloseModal
aria-label={t('Close the share modal')}
onClick={onClose}
tabIndex={-1}
/>
</Box>
}
@@ -128,27 +128,6 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
}
}, []);
const handleRowKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key !== 'Enter') {
return;
}
const target = e.target as HTMLElement | null;
if (
!target ||
!(
target.classList.contains('c__tree-view--row') ||
target.classList.contains('c__tree-view--node')
)
) {
return;
}
e.currentTarget
.querySelector<HTMLDivElement>('.c__tree-view--node')
?.click();
}, []);
/**
* This effect is used to reset the tree when a new document
* that is not part of the current tree is loaded.
@@ -252,7 +231,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
{/* Keyboard instructions for screen readers */}
<Box id="doc-tree-keyboard-instructions" className="sr-only">
{t(
'Use arrow keys to navigate between documents. Press Enter to open a document. Press F2 to focus the emoji button when available, then press F2 again to access document actions. Press F6 to return focus from the document to the tree.',
'Use arrow keys to navigate between documents. Press Enter to open a document. Press F2 to focus the emoji button when available, then press F2 again to access document actions.',
)}
</Box>
<Box
@@ -378,9 +357,6 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
}}
rootNodeId={treeContext.root.id}
renderNode={DocSubPageItem}
rowProps={{
onKeyDown: handleRowKeyDown,
}}
/>
</Overlayer>
)}
@@ -12,8 +12,8 @@ import { useResponsiveStore } from '@/stores';
import { HEADER_HEIGHT } from '../conf';
import { ButtonTogglePanel } from './ButtonTogglePanel';
import { LaGaufre } from './LaGaufre';
import { Title } from './Title';
import { Waffle } from './Waffle';
export const Header = () => {
const { t } = useTranslation();
@@ -85,7 +85,7 @@ export const Header = () => {
</StyledLink>
{!isDesktop ? (
<Box $direction="row" $gap={spacingsTokens['sm']}>
<Waffle />
<LaGaufre />
</Box>
) : (
<Box
@@ -96,7 +96,7 @@ export const Header = () => {
>
<ButtonLogin />
<LanguagePicker />
<Waffle />
<LaGaufre />
</Box>
)}
</Box>
@@ -0,0 +1,39 @@
import { Gaufre } from '@gouvfr-lasuite/integration';
import '@gouvfr-lasuite/integration/dist/css/gaufre.css';
import Script from 'next/script';
import React from 'react';
import { createGlobalStyle } from 'styled-components';
import { useCunninghamTheme } from '@/cunningham';
const GaufreStyle = createGlobalStyle`
.lasuite-gaufre-btn{
box-shadow: inset 0 0 0 0 !important;
border-radius: var(--c--components--button--border-radius) !important;
transition: all var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out) !important;
&:hover, &:focus-visible {
background: var(--c--contextuals--background--semantic--contextual--primary) !important;
}
color: var(--c--contextuals--content--semantic--brand--tertiary) !important;
}
`;
export const LaGaufre = () => {
const { componentTokens } = useCunninghamTheme();
if (!componentTokens['la-gaufre']) {
return null;
}
return (
<>
<Script
src="https://integration.lasuite.numerique.gouv.fr/api/v1/gaufre.js"
strategy="lazyOnload"
id="lasuite-gaufre-script"
/>
<GaufreStyle />
<Gaufre variant="small" />
</>
);
};
@@ -1,48 +0,0 @@
import { LaGaufreV2, LaGaufreV2Props } from '@gouvfr-lasuite/ui-kit';
import React from 'react';
import { css } from 'styled-components';
import { Box } from '@/components';
import { useConfig } from '@/core';
type WaffleAPIType = {
apiUrl: LaGaufreV2Props['apiUrl'];
data?: never;
};
type WaffleDataType = {
apiUrl?: never;
data?: LaGaufreV2Props['data'];
};
export type WaffleType = Omit<
LaGaufreV2Props,
'apiUrl' | 'data' | 'widgetPath'
> &
(WaffleAPIType | WaffleDataType) & {
widgetPath?: string;
};
const LaGaufreV2Fixed = LaGaufreV2 as React.ComponentType<WaffleType>;
export const Waffle = () => {
const { data: conf } = useConfig();
const waffleConfig = conf?.theme_customization?.waffle;
if (!waffleConfig?.apiUrl && !waffleConfig?.data) {
return null;
}
return (
<Box
$css={css`
& > div {
display: flex;
}
`}
>
<LaGaufreV2Fixed {...waffleConfig} />
</Box>
);
};
@@ -1,4 +1,4 @@
export * from './ButtonTogglePanel';
export * from './Header';
export * from './Waffle';
export * from './LaGaufre';
export * from './Title';
@@ -4,7 +4,7 @@ import { Box } from '@/components';
import { useConfig } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { ButtonTogglePanel, Title } from '@/features/header/';
import { Waffle } from '@/features/header/components/Waffle';
import { LaGaufre } from '@/features/header/components/LaGaufre';
import { LanguagePicker } from '@/features/language';
import { useResponsiveStore } from '@/stores';
@@ -81,7 +81,7 @@ export const HomeHeader = () => {
{!isSmallMobile && (
<Box $direction="row" $gap="1rem" $align="center">
<LanguagePicker />
<Waffle />
<LaGaufre />
</Box>
)}
</Box>
@@ -72,14 +72,7 @@ export const ResizableLeftPanel = ({
<Panel
ref={ref}
order={0}
defaultSize={
isDesktop
? Math.max(
minPanelSizePercent,
Math.min(panelSizePercent, maxPanelSizePercent),
)
: 0
}
defaultSize={isDesktop ? panelSizePercent : 0}
minSize={isDesktop ? minPanelSizePercent : 0}
maxSize={isDesktop ? maxPanelSizePercent : 0}
onResize={handleResize}
@@ -779,12 +779,12 @@
"Document deleted": "Document supprimé",
"Document duplicated successfully!": "Document dupliqué avec succès !",
"Document editor": "Éditeur de document",
"Document emoji": "Émoji de document",
"Document owner": "Propriétaire du document",
"Document role text": "Texte du rôle du document",
"Document sections": "Sections du document",
"Document title": "Titre du document",
"Document tree": "Arborescence du document",
"Document viewer": "Visualiseur de document",
"Document visibility": "Visibilité du document",
"Documents grid": "Grille des documents",
"Docx": "Docx",
@@ -826,7 +826,6 @@
"Image 403": "Image 403",
"Image: {{title}}": "Image : {{title}}",
"Insufficient access rights to view the document.": "Droits d'accès insuffisants pour voir le document.",
"Invalid or missing PDF file.": "Fichier PDF non valide ou manquant.",
"Invite": "Inviter",
"Invite new members": "Inviter de nouveaux membres",
"Invite {{count}} members_many": "Inviter {{count}} membres",
@@ -885,7 +884,6 @@
"Others are editing. Your network prevent changes.": "D'autres sont en cours d'édition. Votre réseau empêche les changements.",
"Owner": "Propriétaire",
"PDF": "PDF",
"PDF document": "Document PDF",
"Page Not Found - Error 404": "Page introuvable - Erreur 404",
"Pending invitations": "Invitations en attente",
"People with access via the parent document": "Personnes ayant accès au document parent",
@@ -1224,6 +1222,7 @@
"Document sections": "Document secties",
"Document title": "Documenttitel",
"Document tree": "Boomstructuur document",
"Document viewer": "Document-viewer",
"Document visibility": "Document toegankelijkheid",
"Documents grid": "Documenten overzicht",
"Docx": "Docx",
@@ -1265,7 +1264,6 @@
"Image 403": "Afbeelding 403",
"Image: {{title}}": "Afbeelding: {{title}}",
"Insufficient access rights to view the document.": "Onvoldoende toegangsrechten om het document te bekijken.",
"Invalid or missing PDF file.": "Ongeldig of ontbrekend PDF-bestand.",
"Invite": "Uitnodigen",
"Invite new members": "Nieuwe leden uitnodigen",
"Invite {{count}} members_many": "Nodig {{count}} leden uit",
@@ -1324,7 +1322,6 @@
"Others are editing. Your network prevent changes.": "Anderen zijn aan het bewerken. Uw netwerk voorkomt wijzigingen.",
"Owner": "Eigenaar",
"PDF": "PDF",
"PDF document": "Pdf-document",
"Page Not Found - Error 404": "Pagina niet gevonden - Fout 404",
"Pending invitations": "Openstaande uitnodigingen",
"People with access via the parent document": "Gebruikers met toegang via het bovenliggend document",
@@ -1519,12 +1516,12 @@
"Document deleted": "Документ удалён",
"Document duplicated successfully!": "Документ успешно дублирован!",
"Document editor": "Редактор документа",
"Document emoji": "Эмодзи документа",
"Document owner": "Владелец документа",
"Document role text": "Текст роли документа",
"Document sections": "Разделы документа",
"Document title": "Заголовок документа",
"Document tree": "Иерархия документа",
"Document viewer": "Просмотрщик документа",
"Document visibility": "Видимость документа",
"Documents grid": "Сетка документов",
"Docx": "Docx",
@@ -1566,7 +1563,6 @@
"Image 403": "Изображение 403",
"Image: {{title}}": "Изображение: {{title}}",
"Insufficient access rights to view the document.": "Недостаточно прав доступа для просмотра документа.",
"Invalid or missing PDF file.": "Повреждённый или отсутствующий PDF-файл.",
"Invite": "Приглашение",
"Invite new members": "Пригласить новых участников",
"Invite {{count}} members_many": "Пригласить {{count}} участников",
@@ -1625,7 +1621,6 @@
"Others are editing. Your network prevent changes.": "Другие участники редактируют этот документ. Ваша сеть не позволяет вам присоединиться.",
"Owner": "Владелец",
"PDF": "PDF",
"PDF document": "Документ PDF",
"Page Not Found - Error 404": "Страница не найдена - ошибка 404",
"Pending invitations": "Ожидающие приглашения",
"People with access via the parent document": "Люди, имеющие доступ через родительский документ",
@@ -1923,12 +1918,12 @@
"Document deleted": "Документ видалено",
"Document duplicated successfully!": "Документ успішно продубльовано!",
"Document editor": "Редактор документа",
"Document emoji": "Емодзі документу",
"Document owner": "Власник документа",
"Document role text": "Текст ролі документа",
"Document sections": "Розділи документу",
"Document title": "Назва документа",
"Document tree": "Дерево документа",
"Document viewer": "Переглядач документа",
"Document visibility": "Видимість документа",
"Documents grid": "Сітка документів",
"Docx": "Docx",
@@ -1970,7 +1965,6 @@
"Image 403": "Зображення 403",
"Image: {{title}}": "Зображення: {{title}}",
"Insufficient access rights to view the document.": "Недостатньо прав для перегляду документа.",
"Invalid or missing PDF file.": "Неприпустимий або відсутній PDF-файл.",
"Invite": "Запрошення",
"Invite new members": "Запросити нових учасників",
"Invite {{count}} members_many": "Запросити {{count}} учасників",
@@ -2029,7 +2023,6 @@
"Others are editing. Your network prevent changes.": "Інші учасники редагують документ. Ваша мережа не дозволяє вам вносити зміни.",
"Owner": "Власник",
"PDF": "PDF",
"PDF document": "Документ PDF",
"Page Not Found - Error 404": "Сторінку не знайдено - Помилка 404",
"Pending invitations": "Запрошення в очікуванні",
"People with access via the parent document": "Люди з доступом через батьківський документ",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "impress",
"version": "4.4.0",
"version": "4.3.0",
"private": true,
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-docs",
"version": "4.4.0",
"version": "4.3.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "4.4.0",
"version": "4.3.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "server-y-provider",
"version": "4.4.0",
"version": "4.3.0",
"description": "Y.js provider for docs",
"repository": "https://github.com/suitenumerique/docs",
"license": "MIT",
@@ -139,30 +139,5 @@
"src": "/assets/icon-docs.svg",
"width": "32px"
}
},
"waffle": {
"data": {
"services": [
{
"name": "Docs",
"url": "https://docs.numerique.gouv.fr/",
"maturity": "stable",
"logo": "https://lasuite.numerique.gouv.fr/assets/products/docs.svg"
},
{
"name": "Visio",
"url": "https://visio.numerique.gouv.fr/",
"maturity": "stable",
"logo": "https://lasuite.numerique.gouv.fr/assets/products/visio.svg"
},
{
"name": "Fichiers",
"url": "https://fichiers.numerique.gouv.fr/",
"maturity": "stable",
"logo": "https://lasuite.numerique.gouv.fr/assets/products/fichiers.svg"
}
]
},
"showMoreLimit": 9
}
}
+2 -2
View File
@@ -1,10 +1,10 @@
environments:
dev:
values:
- version: 4.4.0
- version: 4.3.0
feature:
values:
- version: 4.4.0
- version: 4.3.0
feature: ci
domain: example.com
imageTag: demo
+1 -1
View File
@@ -1,5 +1,5 @@
apiVersion: v2
type: application
name: docs
version: 4.4.0
version: 4.3.0
appVersion: latest
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "4.4.0",
"version": "4.3.0",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {