Merge branch 'main' into sbl/user-reconciliation

This commit is contained in:
Sylvain Boissel
2026-01-20 14:49:07 +01:00
committed by GitHub
51 changed files with 3422 additions and 3647 deletions
+38 -12
View File
@@ -6,23 +6,36 @@ 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
- ✨(backend) add field for button label in email template #1817
### Security
- 🔒️(trivy) fix vulnerability about jaraco.context #1806
## [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
### Fixed
- ✅(backend) reduce flakiness on backend test #1769
- 🐛(frontend) fix clickable main content regression #1773
- 🐛(backend) fix TRASHBIN_CUTOFF_DAYS type error #1778
### Security
- 🔒️(backend) validate more strictly url used by cors-proxy endpoint #1768
- ✨(backend) use langfuse to monitor AI actions #1776
### Changed
@@ -30,6 +43,18 @@ and this project adheres to
- ♿(frontend) make html export accessible to screen reader users #1743
- ♿(frontend) add missing label and fix Axes errors to improve a11y #1693
### 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
### 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
### Added
@@ -987,7 +1012,8 @@ 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.3.0...main
[unreleased]: https://github.com/suitenumerique/docs/compare/v4.4.0...main
[v4.4.0]: https://github.com/suitenumerique/docs/releases/v4.4.0
[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
+1 -1
View File
@@ -4,7 +4,7 @@
FROM python:3.13.3-alpine AS base
# Upgrade pip to its latest release to speed up dependencies installation
RUN python -m pip install --upgrade pip setuptools
RUN python -m pip install --upgrade pip
# Upgrade system packages to install security updates
RUN apk update && apk upgrade --no-cache
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+40 -6
View File
@@ -1,4 +1,6 @@
# Runtime Theming 🎨
# Customization Guide 🛠
## Runtime Theming 🎨
### How to Use
@@ -32,7 +34,7 @@ Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom
----
# Runtime JavaScript Injection 🚀
## Runtime JavaScript Injection 🚀
### How to Use
@@ -87,7 +89,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.
@@ -105,7 +107,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.
@@ -128,7 +130,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.
@@ -140,4 +142,36 @@ 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
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
+7
View File
@@ -31,6 +31,13 @@
"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 -1
View File
@@ -1096,7 +1096,8 @@ class Document(MP_Node, BaseModel):
"document": self,
"domain": domain,
"link": f"{domain}/docs/{self.id}/",
"document_title": self.title or str(_("Untitled Document")),
"link_label": self.title or str(_("Untitled Document")),
"button_label": _("Open"),
"logo_img": settings.EMAIL_LOGO_IMG,
}
)
+2 -2
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "4.3.0"
version = "4.4.0"
authors = [{ "name" = "DINUM", "email" = "[email protected]" }]
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.6.0",
"celery[redis]==5.5.3",
"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 Chromium' }).first(),
thread.getByRole('img', { name: `E2E ${browserName}` }).first(),
).toBeVisible();
await expect(thread.getByText('This is a comment').first()).toBeVisible();
await expect(thread.getByText(`E2E ${browserName}`).first()).toBeVisible();
@@ -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 Chromium')).toBeVisible();
await expect(user.getByText(`E2E ${browserName}`)).toBeVisible();
await expect(user.getByText('Owner')).toBeVisible();
await page
@@ -4,7 +4,6 @@ import {
createDoc,
expectLoginPage,
keyCloakSignIn,
randomName,
updateDocTitle,
verifyDocName,
} from './utils-common';
@@ -20,50 +19,6 @@ 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,45 +59,90 @@ 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 La Gauffre interaction', async ({ page }) => {
test('checks a custom waffle', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
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,
},
},
});
await page.goto('/');
const header = page.locator('header').first();
await expect(
header.getByRole('button', {
name: 'Les services de La Suite numérique',
}),
header.getByRole('button', { name: 'Digital LaSuite services' }),
).toBeVisible();
/**
* La gaufre load a js file from a remote server,
* 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,
* it takes some time to load the file and have the interaction available
*/
await page.waitForTimeout(1500);
await header
.getByRole('button', {
name: 'Les services de La Suite numérique',
name: 'Digital LaSuite services',
})
.click();
await expect(
page.getByRole('link', { name: 'France Transfert' }),
).toBeVisible();
await expect(page.getByRole('link', { name: 'Tchap' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Grist' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Visio' })).toBeVisible();
});
});
@@ -124,11 +169,6 @@ 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,9 +113,6 @@ 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,6 +1,11 @@
import { expect, test } from '@playwright/test';
import { TestLanguage, createDoc, waitForLanguageSwitch } from './utils-common';
import {
TestLanguage,
createDoc,
overrideConfig,
waitForLanguageSwitch,
} from './utils-common';
import { openSuggestionMenu } from './utils-editor';
test.describe('Language', () => {
@@ -107,6 +112,15 @@ 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 });
@@ -126,5 +140,14 @@ 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,7 +224,9 @@ export const updateDocTitle = async (page: Page, title: string) => {
await expect(input).toHaveText('');
await expect(input).toBeVisible();
await input.click();
await input.fill(title);
await input.fill(title, {
force: true,
});
await input.click();
await input.blur();
await verifyDocName(page, title);
@@ -328,6 +330,10 @@ 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.3.0",
"version": "4.4.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
+49 -356
View File
@@ -1,58 +1,53 @@
import { defaultTokens } from '@gouvfr-lasuite/cunningham-react';
import { cunninghamConfig as tokens } from '@gouvfr-lasuite/ui-kit';
import merge from 'lodash/merge';
import {
dsfrGlobals,
getUIKitThemesFromGlobals,
whiteLabelGlobals,
} from '@gouvfr-lasuite/ui-kit';
// 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 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',
},
},
},
});
const themeDefault = {
default: themeWhiteLabelLight['default-light'],
};
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: {
const themesDSFRLight = getUIKitThemesFromGlobals(dsfrGlobals, {
prefix: 'dsfr',
variants: ['light'],
overrides: {
globals: {
font: {
families: {
@@ -68,7 +63,6 @@ const dsfrTheme = {
widthFooter: '220px',
alt: 'Gouvernement Logo',
},
'la-gaufre': true,
'home-proconnect': true,
icon: {
src: '/assets/icon-docs-dsfr.svg',
@@ -82,317 +76,16 @@ const dsfrTheme = {
},
},
},
};
});
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 themesDSFR = {
dsfr: themesDSFRLight['dsfr-light'],
};
const docsTokens = {
...tokens,
themes: {
...tokens.themes,
...dsfrTheme,
...genericTheme,
...themeDefault,
...themesDSFR,
},
};
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "4.3.0",
"version": "4.4.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -68,6 +68,7 @@
"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"
@@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next';
import { Box } from '@/components';
export const PICKER_HEIGHT = 500;
interface EmojiPickerProps {
emojiData: EmojiMartData;
onClickOutside: () => void;
@@ -20,11 +22,18 @@ export const EmojiPicker = ({
}: EmojiPickerProps) => {
const { i18n } = useTranslation();
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Escape') {
onClickOutside();
}
};
const pickerContent = (
<Box $position="absolute" $zIndex={1000} $margin="2rem 0 0 0">
<Box $position="absolute" $zIndex={1000} onKeyDownCapture={handleKeyDown}>
<Picker
data={emojiData}
locale={i18n.resolvedLanguage}
autoFocus
onClickOutside={onClickOutside}
onEmojiSelect={onEmojiSelect}
previewPosition="none"
@@ -3,6 +3,8 @@ 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,13 +4,14 @@ import { Resource } from 'i18next';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { Theme } from '@/cunningham/';
import { FooterType } from '@/features/footer';
import { HeaderType } from '@/features/header';
import { HeaderType, WaffleType } 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 = 'generic';
const DEFAULT_THEME: Theme = 'default';
const defaultTokens = getMergedTokens(DEFAULT_THEME);
const initialState: ThemeStore = {
@@ -93,7 +93,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
useSaveDoc(doc.id, provider.document, isConnectedToCollabServer);
const { i18n } = useTranslation();
const lang = i18n.resolvedLanguage;
let lang = i18n.resolvedLanguage;
if (!lang || !(lang in locales)) {
lang = 'en';
}
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
@@ -12,12 +12,9 @@ import { TFunction } from 'i18next';
import React, { useEffect, useState } from 'react';
import { createGlobalStyle, css } from 'styled-components';
import { Box, BoxButton, Icon } from '@/components';
import { Box, BoxButton, EmojiPicker, Icon, emojidata } 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] {
@@ -97,15 +94,15 @@ const CalloutComponent = ({
`}
>
<CalloutBlockStyle />
<Box $position="relative">
<Box
$position="relative"
$css={css`
align-self: start;
`}
>
<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,5 +1,4 @@
export * from './AccessibleImageBlock';
export * from './CalloutBlock';
export { default as emojidata } from './initEmojiCallout';
export * from './PdfBlock';
export * from './UploadLoaderBlock';
@@ -1,21 +1,23 @@
/* eslint-disable react-hooks/rules-of-hooks */
import {
PartialCustomInlineContentFromConfig,
StyleSchema,
} from '@blocknote/core';
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: '',
},
@@ -27,46 +29,97 @@ export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
},
{
render: ({ editor, inlineContent, updateInlineContent }) => {
const { data: doc } = useDoc({ id: inlineContent.props.docId });
const isEditable = editor.isEditable;
if (!inlineContent.props.docId) {
return null;
}
/**
* Update the content title if the referenced doc title changes
* Should not happen
*/
useEffect(() => {
if (
isEditable &&
doc?.title &&
doc.title !== inlineContent.props.title
) {
updateInlineContent({
type: 'interlinkingLinkInline',
props: {
...inlineContent.props,
title: doc.title,
},
});
}
if (!uuidValidate(inlineContent.props.docId)) {
Sentry.captureException(
new Error(`Invalid docId: ${inlineContent.props.docId}`),
{
extra: { info: 'InterlinkingLinkInlineContent' },
},
);
/**
* ⚠️ 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]);
updateInlineContent({
type: 'interlinkingLinkInline',
props: {
docId: '',
title: '',
},
});
return <LinkSelected {...inlineContent.props} />;
return null;
}
return (
<LinkSelected
docId={inlineContent.props.docId}
title={inlineContent.props.title}
isEditable={editor.isEditable}
updateInlineContent={updateInlineContent}
/>
);
},
},
);
interface LinkSelectedProps {
url: string;
docId: 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;
}
const LinkSelected = ({ url, title }: LinkSelectedProps) => {
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 { colorsTokens } = useCunninghamTheme();
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(title);
@@ -74,7 +127,7 @@ const LinkSelected = ({ url, title }: LinkSelectedProps) => {
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
void router.push(url);
void router.push(`/docs/${docId}/`);
};
return (
@@ -247,7 +247,6 @@ export const SearchPage = ({
{
type: 'interlinkingLinkInline',
props: {
url: `/docs/${doc.id}`,
docId: doc.id,
title: doc.title || untitledDocument,
},
@@ -1,4 +1,3 @@
export * from './BlockNoteEditor';
export * from './DocEditor';
export * from './EmojiPicker';
export * from './custom-blocks/';
@@ -1,16 +1,24 @@
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: `📄${inline.props.title}`,
text: `${emoji || '📄'}${titleWithoutEmoji}`,
bold: true,
}),
],
link: window.location.origin + inline.props.url,
link: window.location.origin + `/docs/${inline.props.docId}/`,
});
};
@@ -1,11 +1,17 @@
import React from 'react';
import { getEmojiAndTitle } from '@/docs/doc-management';
import { DocsExporterODT } from '../types';
export const inlineContentMappingInterlinkingLinkODT: DocsExporterODT['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
(inline) => {
const url = window.location.origin + inline.props.url;
const title = inline.props.title;
if (!inline.props.docId) {
return null;
}
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(inline.props.title);
const url = window.location.origin + `/docs/${inline.props.docId}/`;
// Create ODT hyperlink using React.createElement to avoid TypeScript JSX namespace issues
// Uses the same structure as BlockNote's default link mapping
@@ -18,6 +24,6 @@ export const inlineContentMappingInterlinkingLinkODT: DocsExporterODT['mappings'
xlinkShow: 'replace',
xlinkHref: url,
},
`📄${title}`,
`${emoji || '📄'}${titleWithoutEmoji}`,
);
};
@@ -1,21 +1,29 @@
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 + inline.props.url}
src={window.location.origin + `/docs/${inline.props.docId}/`}
style={{
textDecoration: 'none',
color: 'black',
}}
>
{' '}
<Image src={DocSelectedIcon.src} />{' '}
<Text>{inline.props.title}</Text>{' '}
{emoji || <Image src={DocSelectedIcon.src} />}{' '}
<Text>{titleWithoutEmoji}</Text>{' '}
</Link>
);
};
@@ -1,9 +1,18 @@
import { MouseEvent, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { BoxButton, BoxButtonType, Text, TextType } from '@/components';
import { EmojiPicker, emojidata } from '@/docs/doc-editor/';
import {
Box,
BoxButton,
BoxButtonType,
EmojiPicker,
PICKER_HEIGHT,
Text,
TextType,
emojidata,
} from '@/components';
import { useDocTitleUpdate } from '../hooks/useDocTitleUpdate';
@@ -60,9 +69,24 @@ 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: rect.bottom + window.scrollY + 8,
left: rect.left + window.scrollX,
top: shouldPositionAbove
? rect.top - pickerHeight + ROW_OFFSET_TOP
: rect.bottom + ROW_OFFSET_BOTTOM,
left: rect.left,
});
}
@@ -114,13 +138,13 @@ export const DocIcon = ({
</BoxButton>
{openEmojiPicker &&
createPortal(
<div
style={{
position: 'absolute',
top: pickerPosition.top,
left: pickerPosition.left,
zIndex: 1000,
}}
<Box
$position="fixed"
$css={css`
top: ${pickerPosition.top}px;
left: ${pickerPosition.left}px;
z-index: 1000;
`}
>
<EmojiPicker
emojiData={emojidata}
@@ -128,7 +152,7 @@ export const DocIcon = ({
onClickOutside={handleClickOutside}
withOverlay={true}
/>
</div>,
</Box>,
document.body,
)}
</>
@@ -128,6 +128,27 @@ 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.
@@ -357,6 +378,9 @@ 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']}>
<LaGaufre />
<Waffle />
</Box>
) : (
<Box
@@ -96,7 +96,7 @@ export const Header = () => {
>
<ButtonLogin />
<LanguagePicker />
<LaGaufre />
<Waffle />
</Box>
)}
</Box>
@@ -1,39 +0,0 @@
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" />
</>
);
};
@@ -0,0 +1,48 @@
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 './LaGaufre';
export * from './Waffle';
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 { LaGaufre } from '@/features/header/components/LaGaufre';
import { Waffle } from '@/features/header/components/Waffle';
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 />
<LaGaufre />
<Waffle />
</Box>
)}
</Box>
@@ -72,7 +72,14 @@ export const ResizableLeftPanel = ({
<Panel
ref={ref}
order={0}
defaultSize={isDesktop ? panelSizePercent : 0}
defaultSize={
isDesktop
? Math.max(
minPanelSizePercent,
Math.min(panelSizePercent, maxPanelSizePercent),
)
: 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,6 +826,7 @@
"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",
@@ -884,6 +885,7 @@
"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",
@@ -1222,7 +1224,6 @@
"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",
@@ -1264,6 +1265,7 @@
"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",
@@ -1322,6 +1324,7 @@
"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",
@@ -1516,12 +1519,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",
@@ -1563,6 +1566,7 @@
"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}} участников",
@@ -1621,6 +1625,7 @@
"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": "Люди, имеющие доступ через родительский документ",
@@ -1918,12 +1923,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",
@@ -1965,6 +1970,7 @@
"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}} учасників",
@@ -2023,6 +2029,7 @@
"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.3.0",
"version": "4.4.0",
"private": true,
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-docs",
"version": "4.3.0",
"version": "4.4.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "4.3.0",
"version": "4.4.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.3.0",
"version": "4.4.0",
"description": "Y.js provider for docs",
"repository": "https://github.com/suitenumerique/docs",
"license": "MIT",
@@ -139,5 +139,30 @@
"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.3.0
- version: 4.4.0
feature:
values:
- version: 4.3.0
- version: 4.4.0
feature: ci
domain: example.com
imageTag: demo
+1 -1
View File
@@ -1,5 +1,5 @@
apiVersion: v2
type: application
name: docs
version: 4.3.0
version: 4.4.0
appVersion: latest
+3 -3
View File
@@ -22,7 +22,7 @@
<!-- Main Message -->
<mj-text>
{{message|capfirst}}
<a href="{{link}}">{{document_title}}</a>
<a href="{{link}}">{{link_label}}</a>
</mj-text>
<mj-button
href="{{link}}"
@@ -30,7 +30,7 @@
color="white"
padding-bottom="30px"
>
{% trans "Open"%}
{{button_label}}
</mj-button>
<mj-divider
border-width="1px"
@@ -56,4 +56,4 @@
</mj-section>
</mj-wrapper>
</mj-body>
</mjml>
</mjml>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "4.3.0",
"version": "4.4.0",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {