Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3d27793a1 | |||
| 2ebb1d0e91 | |||
| 0cf8b9da1a | |||
| 7be761ce84 | |||
| 5181bba083 | |||
| f434d78b5d | |||
| e07f709dd4 | |||
| afbacb0a24 | |||
| 409e073192 | |||
| 886dcb75d5 | |||
| bb4d2a9fea | |||
| 5e5054282e | |||
| f497e75426 | |||
| 97ab13ded6 | |||
| 99d674c615 | |||
| 1cdb6b62c8 | |||
| 2bf53301d2 | |||
| ec84f31bc7 | |||
| 7813219b86 |
@@ -8,6 +8,22 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- ⚡️(frontend) improve accessibility:
|
||||
- #1248
|
||||
- #1235
|
||||
- #1255
|
||||
- #1262
|
||||
- #1244
|
||||
- #1270
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(makefile) Windows compatibility fix for Docker volume mounting #1264
|
||||
- 🐛(minio) fix user permission error with Minio and Windows #1264
|
||||
|
||||
|
||||
## [3.5.0] - 2025-07-31
|
||||
|
||||
### Added
|
||||
|
||||
@@ -35,9 +35,13 @@ DB_PORT = 5432
|
||||
|
||||
# -- Docker
|
||||
# Get the current user ID to use for docker run and docker exec commands
|
||||
DOCKER_UID = $(shell id -u)
|
||||
DOCKER_GID = $(shell id -g)
|
||||
DOCKER_USER = $(DOCKER_UID):$(DOCKER_GID)
|
||||
ifeq ($(OS),Windows_NT)
|
||||
DOCKER_USER := 0:0 # run containers as root on Windows
|
||||
else
|
||||
DOCKER_UID := $(shell id -u)
|
||||
DOCKER_GID := $(shell id -g)
|
||||
DOCKER_USER := $(DOCKER_UID):$(DOCKER_GID)
|
||||
endif
|
||||
COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose
|
||||
COMPOSE_E2E = DOCKER_USER=$(DOCKER_USER) docker compose -f compose.yml -f compose-e2e.yml
|
||||
COMPOSE_EXEC = $(COMPOSE) exec
|
||||
@@ -48,7 +52,7 @@ COMPOSE_RUN_CROWDIN = $(COMPOSE_RUN) crowdin crowdin
|
||||
|
||||
# -- Backend
|
||||
MANAGE = $(COMPOSE_RUN_APP) python manage.py
|
||||
MAIL_YARN = $(COMPOSE_RUN) -w /app/src/mail node yarn
|
||||
MAIL_YARN = $(COMPOSE_RUN) -w //app/src/mail node yarn
|
||||
|
||||
# -- Frontend
|
||||
PATH_FRONT = ./src/frontend
|
||||
|
||||
@@ -38,6 +38,10 @@ function _set_user() {
|
||||
# options: docker compose command options
|
||||
# ARGS : docker compose command arguments
|
||||
function _docker_compose() {
|
||||
# Set DOCKER_USER for Windows compatibility with MinIO
|
||||
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || -n "${WSL_DISTRO_NAME:-}" ]]; then
|
||||
export DOCKER_USER="0:0"
|
||||
fi
|
||||
|
||||
echo "🐳(compose) file: '${COMPOSE_FILE}'"
|
||||
docker compose \
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
```bash
|
||||
mkdir keycloak
|
||||
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/keycloak/compose.yaml
|
||||
curl -o env.d/kc_postgresql https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/kc_postgresql
|
||||
curl -o env.d/keycloak https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/keycloak
|
||||
curl -o keycloak/compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/keycloak/compose.yaml
|
||||
curl -o keycloak/env.d/kc_postgresql https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/kc_postgresql
|
||||
curl -o keycloak/env.d/keycloak https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/keycloak
|
||||
```
|
||||
|
||||
### Step 2:. Update `env.d/` files
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
```bash
|
||||
mkdir minio
|
||||
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/minio/compose.yaml
|
||||
curl -o minio/compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/minio/compose.yaml
|
||||
```
|
||||
|
||||
### Step 2:. Update compose file with your own values
|
||||
|
||||
@@ -13,7 +13,7 @@ Acme-companion is a lightweight companion container for nginx-proxy. It handles
|
||||
|
||||
```bash
|
||||
mkdir nginx-proxy
|
||||
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/nginx-proxy/compose.yaml
|
||||
curl -o nginx-proxy/compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/nginx-proxy/compose.yaml
|
||||
```
|
||||
|
||||
### Step 2: Edit `DEFAULT_EMAIL` in the compose file.
|
||||
|
||||
@@ -46,9 +46,6 @@ backend:
|
||||
DB_USER: dinum
|
||||
DB_PASSWORD: pass
|
||||
DB_PORT: 5432
|
||||
POSTGRES_DB: impress
|
||||
POSTGRES_USER: dinum
|
||||
POSTGRES_PASSWORD: pass
|
||||
REDIS_URL: redis://default:pass@redis-master:6379/1
|
||||
AWS_S3_ENDPOINT_URL: http://minio.impress.svc.cluster.local:9000
|
||||
AWS_S3_ACCESS_KEY_ID: root
|
||||
|
||||
@@ -31,11 +31,17 @@ For older versions of Docker Engine that do not include Docker Compose:
|
||||
|
||||
```bash
|
||||
mkdir -p docs/env.d
|
||||
cd docs
|
||||
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/compose.yaml
|
||||
curl -o env.d/common https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/common
|
||||
curl -o env.d/backend https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/backend
|
||||
curl -o env.d/yprovider https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/yprovider
|
||||
curl -o env.d/common https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/postgresql
|
||||
curl -o env.d/postgresql https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/postgresql
|
||||
```
|
||||
|
||||
If you are using the sample nginx-proxy configuration:
|
||||
```bash
|
||||
curl -o default.conf.template https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docker/files/production/etc/nginx/conf.d/default.conf.template
|
||||
```
|
||||
|
||||
## Step 2: Configuration
|
||||
|
||||
@@ -168,9 +168,6 @@ DB_NAME: impress
|
||||
DB_USER: dinum
|
||||
DB_PASSWORD: pass
|
||||
DB_PORT: 5432
|
||||
POSTGRES_DB: impress
|
||||
POSTGRES_USER: dinum
|
||||
POSTGRES_PASSWORD: pass
|
||||
```
|
||||
|
||||
### Find s3 bucket connection values
|
||||
|
||||
@@ -83,55 +83,6 @@ If you already have CRLF line endings in your local repository, the **best appro
|
||||
git commit -m "✏️(project) Fix line endings to LF"
|
||||
```
|
||||
|
||||
## Minio Permission Issues on Windows
|
||||
|
||||
### Problem Description
|
||||
|
||||
On Windows, you may encounter permission-related errors when running Minio in development mode with Docker Compose. This typically happens because:
|
||||
|
||||
- **Windows file permissions** don't map well to Unix-style user IDs used in Docker containers
|
||||
- **Docker Desktop** may have issues with user mapping when using the `DOCKER_USER` environment variable
|
||||
- **Minio container** fails to start or access volumes due to permission conflicts
|
||||
|
||||
### Common Symptoms
|
||||
|
||||
- Minio container fails to start with permission denied errors
|
||||
- Error messages related to file system permissions in Minio logs
|
||||
- Unable to create or access buckets in the development environment
|
||||
- Docker Compose showing Minio service as unhealthy or exited
|
||||
|
||||
### Solution for Windows Users
|
||||
|
||||
If you encounter Minio permission issues on Windows, you can temporarily disable user mapping for the Minio service:
|
||||
|
||||
1. **Open the `compose.yml` file**
|
||||
|
||||
2. **Comment out the user directive** in the `minio` service section:
|
||||
```yaml
|
||||
minio:
|
||||
# user: ${DOCKER_USER:-1000} # Comment this line on Windows if permission issues occur
|
||||
image: minio/minio
|
||||
environment:
|
||||
- MINIO_ROOT_USER=impress
|
||||
- MINIO_ROOT_PASSWORD=password
|
||||
# ... rest of the configuration
|
||||
```
|
||||
|
||||
3. **Restart the services**:
|
||||
```bash
|
||||
make run
|
||||
```
|
||||
|
||||
### Why This Works
|
||||
|
||||
- Commenting out the `user` directive allows the Minio container to run with its default user
|
||||
- This bypasses Windows-specific permission mapping issues
|
||||
- The container will have the necessary permissions to access and manage the mounted volumes
|
||||
|
||||
### Note
|
||||
|
||||
This is a **development-only workaround**. In production environments, proper user mapping and security considerations should be maintained according to your deployment requirements.
|
||||
|
||||
## Frontend File Watching Issues on Windows
|
||||
|
||||
### Problem Description
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Y_PROVIDER_API_BASE_URL=http://${YPROVIDER_HOST}:4444/api
|
||||
Y_PROVIDER_API_BASE_URL=http://${YPROVIDER_HOST}:4444/api/
|
||||
Y_PROVIDER_API_KEY=<generate a random key>
|
||||
COLLABORATION_SERVER_SECRET=<generate a random key>
|
||||
COLLABORATION_SERVER_ORIGIN=https://${DOCS_HOST}
|
||||
|
||||
@@ -11,12 +11,14 @@ COPY ./src/frontend/package.json ./package.json
|
||||
COPY ./src/frontend/yarn.lock ./yarn.lock
|
||||
COPY ./src/frontend/apps/impress/package.json ./apps/impress/package.json
|
||||
COPY ./src/frontend/packages/eslint-config-impress/package.json ./packages/eslint-config-impress/package.json
|
||||
COPY ./src/frontend/packages/common/package.json ./packages/common/package.json
|
||||
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
COPY .dockerignore ./.dockerignore
|
||||
COPY ./src/frontend/.prettierrc.js ./.prettierrc.js
|
||||
COPY ./src/frontend/packages/eslint-config-impress ./packages/eslint-config-impress
|
||||
COPY ./src/frontend/packages/common ./packages/common
|
||||
COPY ./src/frontend/apps/impress ./apps/impress
|
||||
|
||||
### ---- Front-end builder image ----
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(
|
||||
page.locator('header').first().locator('h2').getByText('Docs'),
|
||||
page.locator('header').first().locator('h1').getByText('Docs'),
|
||||
).toBeVisible();
|
||||
await page.goto('unknown-page404');
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ const saveStorageState = async (
|
||||
page.locator('header').first().getByRole('button', {
|
||||
name: 'Logout',
|
||||
}),
|
||||
).toBeVisible();
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await page.context().storageState({
|
||||
path: storageState as string,
|
||||
|
||||
@@ -22,7 +22,7 @@ test.describe('Doc Create', () => {
|
||||
);
|
||||
|
||||
const header = page.locator('header').first();
|
||||
await header.locator('h2').getByText('Docs').click();
|
||||
await header.locator('h1').getByText('Docs').click();
|
||||
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid).toBeVisible();
|
||||
|
||||
@@ -4,7 +4,13 @@ import { expect, test } from '@playwright/test';
|
||||
import cs from 'convert-stream';
|
||||
import pdf from 'pdf-parse';
|
||||
|
||||
import { createDoc, verifyDocName } from './utils-common';
|
||||
import {
|
||||
TestLanguage,
|
||||
createDoc,
|
||||
randomName,
|
||||
verifyDocName,
|
||||
waitForLanguageSwitch,
|
||||
} from './utils-common';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -413,6 +419,73 @@ test.describe('Doc Export', () => {
|
||||
expect(pdfData.text).toContain('Column 3');
|
||||
});
|
||||
|
||||
test('it injects the correct language attribute into PDF export', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await waitForLanguageSwitch(page, TestLanguage.French);
|
||||
|
||||
// Wait for the page to be ready after language switch
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
const header = page.locator('header').first();
|
||||
await header.locator('h1').getByText('Docs').click();
|
||||
|
||||
const randomDocFrench = randomName(
|
||||
'doc-language-export-french',
|
||||
browserName,
|
||||
1,
|
||||
)[0];
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Nouveau doc',
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.waitForURL('**/docs/**', {
|
||||
timeout: 10000,
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
const input = page.getByLabel('doc title input');
|
||||
await expect(input).toBeVisible();
|
||||
await expect(input).toHaveText('');
|
||||
await input.click();
|
||||
await input.fill(randomDocFrench);
|
||||
await input.blur();
|
||||
|
||||
const editor = page.locator('.ProseMirror.bn-editor');
|
||||
await editor.click();
|
||||
await editor.fill('Contenu de test pour export en français');
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'download',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDocFrench}.pdf`);
|
||||
});
|
||||
|
||||
void page
|
||||
.getByRole('button', {
|
||||
name: 'Télécharger',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDocFrench}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfString = pdfBuffer.toString('latin1');
|
||||
|
||||
expect(pdfString).toContain('/Lang (fr)');
|
||||
});
|
||||
|
||||
test('it exports the doc with interlinking', async ({
|
||||
page,
|
||||
browserName,
|
||||
|
||||
@@ -8,9 +8,9 @@ test.describe('Doc grid dnd', () => {
|
||||
await page.goto('/');
|
||||
const header = page.locator('header').first();
|
||||
await createDoc(page, 'Draggable doc', browserName, 1);
|
||||
await header.locator('h2').getByText('Docs').click();
|
||||
await header.locator('h1').getByText('Docs').click();
|
||||
await createDoc(page, 'Droppable doc', browserName, 1);
|
||||
await header.locator('h2').getByText('Docs').click();
|
||||
await header.locator('h1').getByText('Docs').click();
|
||||
|
||||
const response = await page.waitForResponse(
|
||||
(response) =>
|
||||
|
||||
@@ -119,7 +119,7 @@ test.describe('Document grid item options', () => {
|
||||
await page.getByText('push_pin').click();
|
||||
|
||||
// Check is pinned
|
||||
await expect(row.getByLabel('Pin document icon')).toBeVisible();
|
||||
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
|
||||
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
|
||||
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
|
||||
|
||||
@@ -128,7 +128,7 @@ test.describe('Document grid item options', () => {
|
||||
await page.getByText('Unpin').click();
|
||||
|
||||
// Check is unpinned
|
||||
await expect(row.getByLabel('Pin document icon')).toBeHidden();
|
||||
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
|
||||
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
|
||||
});
|
||||
|
||||
@@ -227,18 +227,18 @@ test.describe('Documents filters', () => {
|
||||
|
||||
// Initial state
|
||||
await expect(allDocs).toBeVisible();
|
||||
await expect(allDocs).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(allDocs).toHaveAttribute('aria-current', 'page');
|
||||
|
||||
await expect(myDocs).toBeVisible();
|
||||
await expect(myDocs).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
||||
await expect(myDocs).toHaveAttribute('aria-selected', 'false');
|
||||
await expect(myDocs).not.toHaveAttribute('aria-current');
|
||||
|
||||
await expect(sharedWithMe).toBeVisible();
|
||||
await expect(sharedWithMe).toHaveCSS(
|
||||
'background-color',
|
||||
'rgba(0, 0, 0, 0)',
|
||||
);
|
||||
await expect(sharedWithMe).toHaveAttribute('aria-selected', 'false');
|
||||
await expect(sharedWithMe).not.toHaveAttribute('aria-current');
|
||||
|
||||
await allDocs.click();
|
||||
|
||||
|
||||
@@ -409,7 +409,7 @@ test.describe('Doc Header', () => {
|
||||
const row = await getGridRow(page, docTitle);
|
||||
|
||||
// Check is pinned
|
||||
await expect(row.getByLabel('Pin document icon')).toBeVisible();
|
||||
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
|
||||
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
|
||||
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
|
||||
|
||||
@@ -424,7 +424,7 @@ test.describe('Doc Header', () => {
|
||||
await page.goto('/');
|
||||
|
||||
// Check is unpinned
|
||||
await expect(row.getByLabel('Pin document icon')).toBeHidden();
|
||||
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
|
||||
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
|
||||
});
|
||||
|
||||
|
||||
@@ -244,9 +244,7 @@ test.describe('Document create member: Multiple login', () => {
|
||||
|
||||
await keyCloakSignIn(page, otherBrowser!);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId('header-logo-link')).toBeVisible();
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
@@ -271,9 +269,7 @@ test.describe('Document create member: Multiple login', () => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs' }),
|
||||
).toBeVisible({
|
||||
await expect(page.getByTestId('header-logo-link')).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
@@ -334,9 +330,7 @@ test.describe('Document create member: Multiple login', () => {
|
||||
|
||||
await keyCloakSignIn(page, otherBrowser!);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs' }),
|
||||
).toBeVisible({
|
||||
await expect(page.getByTestId('header-logo-link')).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
|
||||
@@ -26,10 +26,7 @@ test.describe('Document search', () => {
|
||||
);
|
||||
await verifyDocName(page, doc2Title);
|
||||
await page.goto('/');
|
||||
await page
|
||||
.getByTestId('left-panel-desktop')
|
||||
.getByRole('button', { name: 'search' })
|
||||
.click();
|
||||
await page.getByTestId('search-docs-button').click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('img', { name: 'No active search' }),
|
||||
@@ -104,9 +101,7 @@ test.describe('Document search', () => {
|
||||
browserName,
|
||||
}) => {
|
||||
// Doc grid filters are not visible
|
||||
const searchButton = page
|
||||
.getByTestId('left-panel-desktop')
|
||||
.getByRole('button', { name: 'search', exact: true });
|
||||
const searchButton = page.getByTestId('search-docs-button');
|
||||
|
||||
const filters = page.getByTestId('doc-search-filters');
|
||||
|
||||
@@ -170,9 +165,7 @@ test.describe('Document search', () => {
|
||||
1,
|
||||
);
|
||||
|
||||
const searchButton = page
|
||||
.getByTestId('left-panel-desktop')
|
||||
.getByRole('button', { name: 'search' });
|
||||
const searchButton = page.getByTestId('search-docs-button');
|
||||
|
||||
await searchButton.click();
|
||||
await page.getByRole('combobox', { name: 'Quick search input' }).click();
|
||||
|
||||
@@ -25,7 +25,7 @@ test.describe('Doc Tree', () => {
|
||||
1,
|
||||
);
|
||||
await verifyDocName(page, titleParent);
|
||||
const addButton = page.getByRole('button', { name: 'New doc' });
|
||||
const addButton = page.getByTestId('new-doc-button');
|
||||
const docTree = page.getByTestId('doc-tree');
|
||||
|
||||
await expect(addButton).toBeVisible();
|
||||
@@ -63,7 +63,7 @@ test.describe('Doc Tree', () => {
|
||||
|
||||
test('check the reorder of sub pages', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'doc-tree-content', browserName, 1);
|
||||
const addButton = page.getByRole('button', { name: 'New doc' });
|
||||
const addButton = page.getByTestId('new-doc-button');
|
||||
await expect(addButton).toBeVisible();
|
||||
|
||||
const docTree = page.getByTestId('doc-tree');
|
||||
@@ -201,7 +201,7 @@ test.describe('Doc Tree', () => {
|
||||
).not.toHaveText(docChild);
|
||||
|
||||
const header = page.locator('header').first();
|
||||
await header.locator('h2').getByText('Docs').click();
|
||||
await header.locator('h1').getByText('Docs').click();
|
||||
await expect(page.getByText(docChild)).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
@@ -122,9 +122,7 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
|
||||
await keyCloakSignIn(page, otherBrowser!);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs' }),
|
||||
).toBeVisible({
|
||||
await expect(page.getByTestId('header-logo-link')).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
@@ -178,9 +176,7 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
|
||||
await keyCloakSignIn(page, otherBrowser!);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId('header-logo-link')).toBeVisible();
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
@@ -246,8 +242,8 @@ test.describe('Doc Visibility: Public', () => {
|
||||
cardContainer.getByText('Public document', { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'search' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible();
|
||||
await expect(page.getByTestId('search-docs-button')).toBeVisible();
|
||||
await expect(page.getByTestId('new-doc-button')).toBeVisible();
|
||||
|
||||
const urlDoc = page.url();
|
||||
|
||||
@@ -262,8 +258,8 @@ test.describe('Doc Visibility: Public', () => {
|
||||
await page.goto(urlDoc);
|
||||
|
||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'search' })).toBeHidden();
|
||||
await expect(page.getByRole('button', { name: 'New doc' })).toBeHidden();
|
||||
await expect(page.getByTestId('search-docs-button')).toBeHidden();
|
||||
await expect(page.getByTestId('new-doc-button')).toBeHidden();
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
||||
const card = page.getByLabel('It is the card information');
|
||||
await expect(card).toBeVisible();
|
||||
@@ -455,9 +451,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
const otherBrowser = BROWSERS.find((b) => b !== browserName);
|
||||
await keyCloakSignIn(page, otherBrowser!);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs' }),
|
||||
).toBeVisible({
|
||||
await expect(page.getByTestId('header-logo-link')).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
@@ -545,9 +539,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
const otherBrowser = BROWSERS.find((b) => b !== browserName);
|
||||
await keyCloakSignIn(page, otherBrowser!);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId('header-logo-link')).toBeVisible();
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ test.describe('Header', () => {
|
||||
|
||||
const header = page.locator('header').first();
|
||||
|
||||
await expect(header.getByLabel('Docs Logo')).toBeVisible();
|
||||
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
|
||||
await expect(header.getByTestId('header-logo-link')).toBeVisible();
|
||||
await expect(header.locator('h1').getByText('Docs')).toHaveCSS(
|
||||
'font-family',
|
||||
/Roboto/i,
|
||||
);
|
||||
@@ -37,8 +37,8 @@ test.describe('Header', () => {
|
||||
|
||||
const header = page.locator('header').first();
|
||||
|
||||
await expect(header.getByLabel('Docs Logo')).toBeVisible();
|
||||
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
|
||||
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
|
||||
await expect(header.locator('h1').getByText('Docs')).toHaveCSS(
|
||||
'font-family',
|
||||
/Marianne/i,
|
||||
);
|
||||
@@ -106,7 +106,7 @@ test.describe('Header mobile', () => {
|
||||
const header = page.locator('header').first();
|
||||
|
||||
await expect(header.getByLabel('Open the header menu')).toBeVisible();
|
||||
await expect(header.getByRole('link', { name: 'Docs Logo' })).toBeVisible();
|
||||
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
|
||||
await expect(
|
||||
header.getByRole('button', {
|
||||
name: 'Les services de La Suite numérique',
|
||||
|
||||
@@ -15,10 +15,13 @@ test.describe('Home page', () => {
|
||||
const header = page.locator('header').first();
|
||||
const footer = page.locator('footer').first();
|
||||
await expect(header).toBeVisible();
|
||||
await expect(
|
||||
header.getByRole('button', { name: /Language/ }),
|
||||
).toBeVisible();
|
||||
await expect(header.getByRole('img', { name: 'Docs logo' })).toBeVisible();
|
||||
|
||||
const languageButton = page.getByRole('button', {
|
||||
name: /Language|Select language/,
|
||||
});
|
||||
await expect(languageButton).toBeVisible();
|
||||
|
||||
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
|
||||
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
|
||||
|
||||
// Check the titles
|
||||
@@ -65,20 +68,31 @@ test.describe('Home page', () => {
|
||||
|
||||
await page.goto('/docs/');
|
||||
|
||||
// Wait for the page to be fully loaded and responsive store to be initialized
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Wait a bit more for the responsive store to be initialized
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Check header content
|
||||
const header = page.locator('header').first();
|
||||
const footer = page.locator('footer').first();
|
||||
await expect(header).toBeVisible();
|
||||
await expect(
|
||||
header.getByRole('button', { name: /Language/ }),
|
||||
).toBeVisible();
|
||||
|
||||
// Check for language picker - it should be visible on desktop
|
||||
// Use a more flexible selector that works with both Header and HomeHeader
|
||||
const languageButton = page.getByRole('button', {
|
||||
name: /Language|Select language/,
|
||||
});
|
||||
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();
|
||||
await expect(header.getByRole('img', { name: 'Docs logo' })).toBeVisible();
|
||||
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
|
||||
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
|
||||
|
||||
// Check the titles
|
||||
|
||||
@@ -1,30 +1,17 @@
|
||||
import { Page, expect, test } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc } from './utils-common';
|
||||
|
||||
test.describe.serial('Language', () => {
|
||||
let page: Page;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
import { TestLanguage, createDoc, waitForLanguageSwitch } from './utils-common';
|
||||
|
||||
test.describe('Language', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForLanguageSwitch(page, TestLanguage.English);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
// Switch back to English - important for other tests to run as expected
|
||||
await waitForLanguageSwitch(page, TestLanguage.English);
|
||||
});
|
||||
|
||||
test('checks language switching', async ({ page }) => {
|
||||
const header = page.locator('header').first();
|
||||
const languagePicker = header.locator('.--docs--language-picker-text');
|
||||
|
||||
await expect(page.locator('html')).toHaveAttribute('lang', 'en-us');
|
||||
|
||||
// initial language should be english
|
||||
await expect(
|
||||
@@ -36,17 +23,57 @@ test.describe.serial('Language', () => {
|
||||
// switch to french
|
||||
await waitForLanguageSwitch(page, TestLanguage.French);
|
||||
|
||||
await expect(page.locator('html')).toHaveAttribute('lang', 'fr');
|
||||
|
||||
await expect(
|
||||
header.getByRole('button').getByText('Français'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByLabel('Se déconnecter')).toBeVisible();
|
||||
|
||||
await header.getByRole('button').getByText('Français').click();
|
||||
await page.getByLabel('Deutsch').click();
|
||||
// Switch to German using the utility function for consistency
|
||||
await waitForLanguageSwitch(page, TestLanguage.German);
|
||||
await expect(header.getByRole('button').getByText('Deutsch')).toBeVisible();
|
||||
|
||||
await expect(page.getByLabel('Abmelden')).toBeVisible();
|
||||
|
||||
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
|
||||
|
||||
await languagePicker.click();
|
||||
|
||||
await expect(page.locator('[role="menu"]')).toBeVisible();
|
||||
|
||||
const menuItems = page.getByRole('menuitem');
|
||||
await expect(menuItems.first()).toBeVisible();
|
||||
|
||||
await menuItems.first().click();
|
||||
|
||||
await expect(page.locator('html')).toHaveAttribute('lang', 'en');
|
||||
await expect(languagePicker).toContainText('English');
|
||||
});
|
||||
test('can switch language using only keyboard', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForLanguageSwitch(page, TestLanguage.English);
|
||||
|
||||
const languagePicker = page.getByRole('button', {
|
||||
name: /select language/i,
|
||||
});
|
||||
|
||||
await expect(languagePicker).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
const menu = page.getByRole('menu');
|
||||
await expect(menu).toBeVisible();
|
||||
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await expect(page.locator('html')).not.toHaveAttribute('lang', 'en-us');
|
||||
});
|
||||
|
||||
test('checks that backend uses the same language as the frontend', async ({
|
||||
@@ -94,48 +121,3 @@ test.describe.serial('Language', () => {
|
||||
await expect(page.getByText('Titres', { exact: true })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// language helper
|
||||
export const TestLanguage = {
|
||||
English: {
|
||||
label: 'English',
|
||||
expectedLocale: ['en-us'],
|
||||
},
|
||||
French: {
|
||||
label: 'Français',
|
||||
expectedLocale: ['fr-fr'],
|
||||
},
|
||||
German: {
|
||||
label: 'Deutsch',
|
||||
expectedLocale: ['de-de'],
|
||||
},
|
||||
} as const;
|
||||
|
||||
type TestLanguageKey = keyof typeof TestLanguage;
|
||||
type TestLanguageValue = (typeof TestLanguage)[TestLanguageKey];
|
||||
|
||||
export async function waitForLanguageSwitch(
|
||||
page: Page,
|
||||
lang: TestLanguageValue,
|
||||
) {
|
||||
const header = page.locator('header').first();
|
||||
const languagePicker = header.locator('.--docs--language-picker-text');
|
||||
const isAlreadyTargetLanguage = await languagePicker
|
||||
.innerText()
|
||||
.then((text) => text.toLowerCase().includes(lang.label.toLowerCase()));
|
||||
|
||||
if (isAlreadyTargetLanguage) {
|
||||
return;
|
||||
}
|
||||
|
||||
await languagePicker.click();
|
||||
const responsePromise = page.waitForResponse(
|
||||
(resp) =>
|
||||
resp.url().includes('/user') && resp.request().method() === 'PATCH',
|
||||
);
|
||||
await page.getByLabel(lang.label).click();
|
||||
const resolvedResponsePromise = await responsePromise;
|
||||
const responseData = await resolvedResponsePromise.json();
|
||||
|
||||
expect(lang.expectedLocale).toContain(responseData.language);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ test.describe('Left panel desktop', () => {
|
||||
test('checks all the elements are visible', async ({ page }) => {
|
||||
await expect(page.getByTestId('left-panel-desktop')).toBeVisible();
|
||||
await expect(page.getByTestId('left-panel-mobile')).toBeHidden();
|
||||
await expect(page.getByRole('button', { name: 'house' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible();
|
||||
await expect(page.getByTestId('home-button')).toBeVisible();
|
||||
await expect(page.getByTestId('new-doc-button')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,9 +27,11 @@ test.describe('Left panel mobile', () => {
|
||||
await expect(page.getByTestId('left-panel-mobile')).not.toBeInViewport();
|
||||
|
||||
const header = page.locator('header').first();
|
||||
const homeButton = page.getByRole('button', { name: 'house' });
|
||||
const newDocButton = page.getByRole('button', { name: 'New doc' });
|
||||
const languageButton = page.getByRole('button', { name: /Language/ });
|
||||
const homeButton = page.getByTestId('home-button');
|
||||
const newDocButton = page.getByTestId('new-doc-button');
|
||||
const languageButton = page.getByRole('button', {
|
||||
name: 'Select language',
|
||||
});
|
||||
const logoutButton = page.getByRole('button', { name: 'Logout' });
|
||||
|
||||
await expect(homeButton).not.toBeInViewport();
|
||||
|
||||
@@ -154,7 +154,7 @@ export const goToGridDoc = async (
|
||||
{ nthRow = 1, title }: GoToGridDocOptions = {},
|
||||
) => {
|
||||
const header = page.locator('header').first();
|
||||
await header.locator('h2').getByText('Docs').click();
|
||||
await header.locator('h1').getByText('Docs').click();
|
||||
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid).toBeVisible();
|
||||
@@ -273,3 +273,52 @@ export const expectLoginPage = async (page: Page) =>
|
||||
).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
// language helper
|
||||
export const TestLanguage = {
|
||||
English: {
|
||||
label: 'English',
|
||||
expectedLocale: ['en-us'],
|
||||
},
|
||||
French: {
|
||||
label: 'Français',
|
||||
expectedLocale: ['fr-fr'],
|
||||
},
|
||||
German: {
|
||||
label: 'Deutsch',
|
||||
expectedLocale: ['de-de'],
|
||||
},
|
||||
} as const;
|
||||
|
||||
type TestLanguageKey = keyof typeof TestLanguage;
|
||||
type TestLanguageValue = (typeof TestLanguage)[TestLanguageKey];
|
||||
|
||||
export async function waitForLanguageSwitch(
|
||||
page: Page,
|
||||
lang: TestLanguageValue,
|
||||
) {
|
||||
await page.route('**/api/v1.0/users/**', async (route, request) => {
|
||||
if (request.method().includes('PATCH')) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
language: lang.expectedLocale[0],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
const header = page.locator('header').first();
|
||||
const languagePicker = header.locator('.--docs--language-picker-text');
|
||||
const isAlreadyTargetLanguage = await languagePicker
|
||||
.innerText()
|
||||
.then((text) => text.toLowerCase().includes(lang.label.toLowerCase()));
|
||||
|
||||
if (isAlreadyTargetLanguage) {
|
||||
return;
|
||||
}
|
||||
|
||||
await languagePicker.click();
|
||||
|
||||
await page.getByLabel(lang.label).click();
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"test:ui::chromium": "yarn test:ui --project=chromium"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.54.1",
|
||||
"@playwright/test": "1.54.2",
|
||||
"@types/node": "*",
|
||||
"@types/pdf-parse": "1.1.5",
|
||||
"eslint-config-impress": "*",
|
||||
|
||||
@@ -8,6 +8,7 @@ const buildId = crypto.randomBytes(256).toString('hex').slice(0, 8);
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
trailingSlash: true,
|
||||
transpilePackages: ['package-docs'],
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
|
||||
@@ -29,12 +29,12 @@
|
||||
"@emoji-mart/react": "1.1.1",
|
||||
"@fontsource/material-icons": "5.2.5",
|
||||
"@gouvfr-lasuite/integration": "1.0.3",
|
||||
"@gouvfr-lasuite/ui-kit": "0.10.0",
|
||||
"@gouvfr-lasuite/ui-kit": "0.11.0",
|
||||
"@hocuspocus/provider": "2.15.2",
|
||||
"@openfun/cunningham-react": "3.2.0",
|
||||
"@openfun/cunningham-react": "3.2.1",
|
||||
"@react-pdf/renderer": "4.3.0",
|
||||
"@sentry/nextjs": "9.42.0",
|
||||
"@tanstack/react-query": "5.83.0",
|
||||
"@sentry/nextjs": "10.2.0",
|
||||
"@tanstack/react-query": "5.84.1",
|
||||
"canvg": "4.0.3",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
@@ -46,8 +46,9 @@
|
||||
"idb": "8.0.3",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.7.1",
|
||||
"next": "15.4.4",
|
||||
"posthog-js": "1.258.2",
|
||||
"next": "15.4.6",
|
||||
"package-docs": "*",
|
||||
"posthog-js": "1.258.6",
|
||||
"react": "*",
|
||||
"react-aria-components": "1.11.0",
|
||||
"react-dom": "*",
|
||||
@@ -58,22 +59,22 @@
|
||||
"use-debounce": "10.0.5",
|
||||
"y-protocols": "1.0.6",
|
||||
"yjs": "*",
|
||||
"zustand": "5.0.6"
|
||||
"zustand": "5.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@tanstack/react-query-devtools": "5.83.0",
|
||||
"@testing-library/dom": "10.4.0",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@tanstack/react-query-devtools": "5.84.1",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/jest-dom": "6.6.4",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/luxon": "3.6.2",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"cross-env": "7.0.3",
|
||||
"cross-env": "10.0.0",
|
||||
"dotenv": "17.2.1",
|
||||
"eslint-config-impress": "*",
|
||||
"fetch-mock": "9.11.0",
|
||||
@@ -81,11 +82,11 @@
|
||||
"jest-environment-jsdom": "30.0.5",
|
||||
"node-fetch": "2.7.0",
|
||||
"prettier": "3.6.2",
|
||||
"stylelint": "16.22.0",
|
||||
"stylelint-config-standard": "38.0.0",
|
||||
"stylelint": "16.23.0",
|
||||
"stylelint-config-standard": "39.0.0",
|
||||
"stylelint-prettier": "5.0.3",
|
||||
"typescript": "*",
|
||||
"webpack": "5.100.2",
|
||||
"webpack": "5.101.0",
|
||||
"workbox-webpack-plugin": "7.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,13 @@ const StyledButton = styled(Button)<StyledButtonProps>`
|
||||
font-size: 0.938rem;
|
||||
padding: 0;
|
||||
${({ $css }) => $css};
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--c--theme--colors--primary-500);
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
transition: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface DropButtonProps {
|
||||
|
||||
+62
-5
@@ -1,10 +1,19 @@
|
||||
import { HorizontalSeparator } from '@gouvfr-lasuite/ui-kit';
|
||||
import { Fragment, PropsWithChildren, useRef, useState } from 'react';
|
||||
import {
|
||||
Fragment,
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
|
||||
|
||||
export type DropdownMenuOption = {
|
||||
icon?: string;
|
||||
label: string;
|
||||
@@ -46,12 +55,40 @@ export const DropdownMenu = ({
|
||||
}: PropsWithChildren<DropdownMenuProps>) => {
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
const [isOpen, setIsOpen] = useState(opened ?? false);
|
||||
const [focusedIndex, setFocusedIndex] = useState(-1);
|
||||
const blockButtonRef = useRef<HTMLDivElement>(null);
|
||||
const menuItemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
const onOpenChange = (isOpen: boolean) => {
|
||||
setIsOpen(isOpen);
|
||||
afterOpenChange?.(isOpen);
|
||||
};
|
||||
const onOpenChange = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
setIsOpen(isOpen);
|
||||
setFocusedIndex(-1);
|
||||
afterOpenChange?.(isOpen);
|
||||
},
|
||||
[afterOpenChange],
|
||||
);
|
||||
|
||||
useDropdownKeyboardNav({
|
||||
isOpen,
|
||||
focusedIndex,
|
||||
options,
|
||||
menuItemRefs,
|
||||
setFocusedIndex,
|
||||
onOpenChange,
|
||||
});
|
||||
|
||||
// Focus selected menu item when menu opens
|
||||
useEffect(() => {
|
||||
if (isOpen && menuItemRefs.current.length > 0) {
|
||||
const selectedIndex = options.findIndex((option) => option.isSelected);
|
||||
if (selectedIndex !== -1) {
|
||||
setFocusedIndex(selectedIndex);
|
||||
setTimeout(() => {
|
||||
menuItemRefs.current[selectedIndex]?.focus();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}, [isOpen, options]);
|
||||
|
||||
if (disabled) {
|
||||
return children;
|
||||
@@ -95,6 +132,7 @@ export const DropdownMenu = ({
|
||||
$maxWidth="320px"
|
||||
$minWidth={`${blockButtonRef.current?.clientWidth}px`}
|
||||
role="menu"
|
||||
aria-label={label}
|
||||
>
|
||||
{topMessage && (
|
||||
<Text
|
||||
@@ -115,14 +153,20 @@ export const DropdownMenu = ({
|
||||
return;
|
||||
}
|
||||
const isDisabled = option.disabled !== undefined && option.disabled;
|
||||
const isFocused = index === focusedIndex;
|
||||
|
||||
return (
|
||||
<Fragment key={option.label}>
|
||||
<BoxButton
|
||||
ref={(el) => {
|
||||
menuItemRefs.current[index] = el;
|
||||
}}
|
||||
role="menuitem"
|
||||
aria-label={option.label}
|
||||
data-testid={option.testId}
|
||||
$direction="row"
|
||||
disabled={isDisabled}
|
||||
$hasTransition={false}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -158,6 +202,19 @@ export const DropdownMenu = ({
|
||||
&:hover {
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--c--theme--colors--primary-500);
|
||||
outline-offset: -2px;
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
}
|
||||
|
||||
${isFocused &&
|
||||
css`
|
||||
outline: 2px solid var(--c--theme--colors--primary-500);
|
||||
outline-offset: -2px;
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
`}
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
@@ -0,0 +1,88 @@
|
||||
import { RefObject, useEffect } from 'react';
|
||||
|
||||
import { DropdownMenuOption } from '../DropdownMenu';
|
||||
|
||||
type UseDropdownKeyboardNavProps = {
|
||||
isOpen: boolean;
|
||||
focusedIndex: number;
|
||||
options: DropdownMenuOption[];
|
||||
menuItemRefs: RefObject<(HTMLDivElement | null)[]>;
|
||||
setFocusedIndex: (index: number) => void;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export const useDropdownKeyboardNav = ({
|
||||
isOpen,
|
||||
focusedIndex,
|
||||
options,
|
||||
menuItemRefs,
|
||||
setFocusedIndex,
|
||||
onOpenChange,
|
||||
}: UseDropdownKeyboardNavProps) => {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const enabledIndices = options
|
||||
.map((option, index) =>
|
||||
option.show !== false && !option.disabled ? index : -1,
|
||||
)
|
||||
.filter((index) => index !== -1);
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
const nextIndex =
|
||||
focusedIndex < enabledIndices.length - 1 ? focusedIndex + 1 : 0;
|
||||
const nextEnabledIndex = enabledIndices[nextIndex];
|
||||
setFocusedIndex(nextIndex);
|
||||
menuItemRefs.current[nextEnabledIndex]?.focus();
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
const prevIndex =
|
||||
focusedIndex > 0 ? focusedIndex - 1 : enabledIndices.length - 1;
|
||||
const prevEnabledIndex = enabledIndices[prevIndex];
|
||||
setFocusedIndex(prevIndex);
|
||||
menuItemRefs.current[prevEnabledIndex]?.focus();
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
event.preventDefault();
|
||||
if (focusedIndex >= 0 && focusedIndex < enabledIndices.length) {
|
||||
const selectedOptionIndex = enabledIndices[focusedIndex];
|
||||
const selectedOption = options[selectedOptionIndex];
|
||||
if (selectedOption && selectedOption.callback) {
|
||||
onOpenChange(false);
|
||||
void selectedOption.callback();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
event.preventDefault();
|
||||
onOpenChange(false);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [
|
||||
isOpen,
|
||||
focusedIndex,
|
||||
options,
|
||||
menuItemRefs,
|
||||
setFocusedIndex,
|
||||
onOpenChange,
|
||||
]);
|
||||
};
|
||||
@@ -1,9 +1,12 @@
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box } from '../Box';
|
||||
import { DropdownMenu, DropdownMenuOption } from '../DropdownMenu';
|
||||
import { Icon } from '../Icon';
|
||||
import { Text } from '../Text';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuOption,
|
||||
} from '../dropdown-menu/DropdownMenu';
|
||||
|
||||
export type FilterDropdownProps = {
|
||||
options: DropdownMenuOption[];
|
||||
|
||||
@@ -3,7 +3,7 @@ export * from './Box';
|
||||
export * from './BoxButton';
|
||||
export * from './Card';
|
||||
export * from './DropButton';
|
||||
export * from './DropdownMenu';
|
||||
export * from './dropdown-menu/DropdownMenu';
|
||||
export * from './quick-search';
|
||||
export * from './Icon';
|
||||
export * from './InfiniteScroll';
|
||||
|
||||
+5
-2
@@ -11,6 +11,7 @@ import { BlockNoteView } from '@blocknote/mantine';
|
||||
import '@blocknote/mantine/style.css';
|
||||
import { useCreateBlockNote } from '@blocknote/react';
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import { createDividerBlock } from 'package-docs';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as Y from 'yjs';
|
||||
@@ -33,7 +34,7 @@ import { randomColor } from '../utils';
|
||||
|
||||
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
|
||||
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
|
||||
import { CalloutBlock, DividerBlock } from './custom-blocks';
|
||||
import { CalloutBlock } from './custom-blocks';
|
||||
import {
|
||||
InterlinkingLinkInlineContent,
|
||||
InterlinkingSearchInlineContent,
|
||||
@@ -49,7 +50,9 @@ const baseBlockNoteSchema = withPageBreak(
|
||||
blockSpecs: {
|
||||
...defaultBlockSpecs,
|
||||
callout: CalloutBlock,
|
||||
divider: DividerBlock,
|
||||
divider: createDividerBlock({
|
||||
color: 'var(--c--theme--colors--greyscale-300)',
|
||||
}),
|
||||
},
|
||||
inlineContentSpecs: {
|
||||
...defaultInlineContentSpecs,
|
||||
|
||||
+10
-5
@@ -6,19 +6,19 @@ import {
|
||||
useBlockNoteEditor,
|
||||
useDictionary,
|
||||
} from '@blocknote/react';
|
||||
import { getDividerReactSlashMenuItems } from 'package-docs';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Icon } from '@/components';
|
||||
|
||||
import {
|
||||
DocsBlockSchema,
|
||||
DocsInlineContentSchema,
|
||||
DocsStyleSchema,
|
||||
} from '../types';
|
||||
|
||||
import {
|
||||
getCalloutReactSlashMenuItems,
|
||||
getDividerReactSlashMenuItems,
|
||||
} from './custom-blocks';
|
||||
import { getCalloutReactSlashMenuItems } from './custom-blocks';
|
||||
import { useGetInterlinkingMenuItems } from './custom-inline-content';
|
||||
import XLMultiColumn from './xl-multi-column';
|
||||
|
||||
@@ -55,7 +55,12 @@ export const BlockNoteSuggestionMenu = () => {
|
||||
getCalloutReactSlashMenuItems(editor, t, basicBlocksName),
|
||||
getMultiColumnSlashMenuItems?.(editor) || [],
|
||||
getPageBreakReactSlashMenuItems(editor),
|
||||
getDividerReactSlashMenuItems(editor, t, basicBlocksName),
|
||||
getDividerReactSlashMenuItems(
|
||||
editor,
|
||||
t,
|
||||
basicBlocksName,
|
||||
<Icon iconName="remove" $size="18px" />,
|
||||
),
|
||||
),
|
||||
query,
|
||||
),
|
||||
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import { insertOrUpdateBlock } from '@blocknote/core';
|
||||
import { createReactBlockSpec } from '@blocknote/react';
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
import { Box, Icon } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../../types';
|
||||
|
||||
export const DividerBlock = createReactBlockSpec(
|
||||
{
|
||||
type: 'divider',
|
||||
propSchema: {},
|
||||
content: 'none',
|
||||
},
|
||||
{
|
||||
render: () => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="hr"
|
||||
$width="100%"
|
||||
$background={colorsTokens['greyscale-300']}
|
||||
$margin="1rem 0"
|
||||
$css={`border: 1px solid ${colorsTokens['greyscale-300']};`}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const getDividerReactSlashMenuItems = (
|
||||
editor: DocsBlockNoteEditor,
|
||||
t: TFunction<'translation', undefined>,
|
||||
group: string,
|
||||
) => [
|
||||
{
|
||||
title: t('Divider'),
|
||||
onItemClick: () => {
|
||||
insertOrUpdateBlock(editor, {
|
||||
type: 'divider',
|
||||
});
|
||||
},
|
||||
aliases: ['divider', 'hr', 'horizontal rule', 'line', 'separator'],
|
||||
group,
|
||||
icon: <Icon iconName="remove" $size="18px" />,
|
||||
subtext: t('Add a horizontal line'),
|
||||
},
|
||||
];
|
||||
-1
@@ -1,2 +1 @@
|
||||
export * from './CalloutBlock';
|
||||
export * from './DividerBlock';
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { DocumentProps, pdf } from '@react-pdf/renderer';
|
||||
import { useMemo, useState } from 'react';
|
||||
import i18next from 'i18next';
|
||||
import { cloneElement, isValidElement, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
@@ -92,10 +93,15 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
const exporter = new PDFExporter(editor.schema, pdfDocsSchemaMappings, {
|
||||
resolveFileUrl: async (url) => exportCorsResolveFileUrl(doc.id, url),
|
||||
});
|
||||
const pdfDocument = (await exporter.toReactPDFDocument(
|
||||
const rawPdfDocument = (await exporter.toReactPDFDocument(
|
||||
exportDocument,
|
||||
)) as React.ReactElement<DocumentProps>;
|
||||
|
||||
// Inject language for screen reader support
|
||||
const pdfDocument = isValidElement(rawPdfDocument)
|
||||
? cloneElement(rawPdfDocument, { language: i18next.language })
|
||||
: rawPdfDocument;
|
||||
|
||||
blobExport = await pdf(pdfDocument).toBlob();
|
||||
} else {
|
||||
const exporter = new DOCXExporter(editor.schema, docxDocsSchemaMappings, {
|
||||
|
||||
@@ -43,9 +43,9 @@ export type DocsExporterPDF = Exporter<
|
||||
>;
|
||||
|
||||
export type DocsExporterDocx = Exporter<
|
||||
NoInfer<DocsBlockSchema>,
|
||||
NoInfer<DocsInlineContentSchema>,
|
||||
NoInfer<DocsStyleSchema>,
|
||||
DocsBlockSchema,
|
||||
DocsInlineContentSchema,
|
||||
DocsStyleSchema,
|
||||
Promise<Paragraph[] | Paragraph | Table> | Paragraph[] | Paragraph | Table,
|
||||
ParagraphChild,
|
||||
IRunPropertiesOptions,
|
||||
|
||||
+3
@@ -52,14 +52,17 @@ export const SimpleDocItem = ({
|
||||
filter: drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.05));
|
||||
`}
|
||||
$padding={`${spacingsTokens['3xs']} 0`}
|
||||
data-testid={isPinned ? `doc-pinned-${doc.id}` : undefined}
|
||||
>
|
||||
{isPinned ? (
|
||||
<PinnedDocumentIcon
|
||||
aria-hidden="true"
|
||||
aria-label={t('Pin document icon')}
|
||||
color={colorsTokens['primary-500']}
|
||||
/>
|
||||
) : (
|
||||
<SimpleFileIcon
|
||||
aria-hidden="true"
|
||||
aria-label={t('Simple document icon')}
|
||||
color={colorsTokens['primary-500']}
|
||||
/>
|
||||
|
||||
+14
-1
@@ -1,5 +1,6 @@
|
||||
import { useModal } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { DropdownMenu, DropdownMenuOption, Icon } from '@/components';
|
||||
import {
|
||||
@@ -75,14 +76,26 @@ export const DocsGridActions = ({
|
||||
},
|
||||
];
|
||||
|
||||
const documentTitle = doc.title || t('Untitled document');
|
||||
const menuLabel = t('Open the menu of actions for the document: {{title}}', {
|
||||
title: documentTitle,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu options={options}>
|
||||
<DropdownMenu options={options} label={menuLabel}>
|
||||
<Icon
|
||||
data-testid={`docs-grid-actions-button-${doc.id}`}
|
||||
iconName="more_horiz"
|
||||
$theme="primary"
|
||||
$variation="600"
|
||||
aria-label={t('More options')}
|
||||
$css={css`
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
|
||||
|
||||
@@ -39,7 +39,11 @@ export const Header = () => {
|
||||
className="--docs--header"
|
||||
>
|
||||
{!isDesktop && <ButtonTogglePanel />}
|
||||
<StyledLink href="/">
|
||||
<StyledLink
|
||||
href="/"
|
||||
data-testid="header-logo-link"
|
||||
aria-label={t('Back to homepage')}
|
||||
>
|
||||
<Box
|
||||
$align="center"
|
||||
$gap={spacingsTokens['3xs']}
|
||||
@@ -49,11 +53,12 @@ export const Header = () => {
|
||||
$margin={{ top: 'auto' }}
|
||||
>
|
||||
<IconDocs
|
||||
aria-label={t('Docs Logo')}
|
||||
data-testid="header-icon-docs"
|
||||
width={32}
|
||||
color={colorsTokens['primary-text']}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Title />
|
||||
<Title headingLevel="h1" aria-hidden="true" />
|
||||
</Box>
|
||||
</StyledLink>
|
||||
{!isDesktop ? (
|
||||
|
||||
@@ -3,7 +3,11 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Box, Text } from '@/components/';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
export const Title = () => {
|
||||
type TitleSemanticsProps = {
|
||||
headingLevel?: 'h1' | 'h2' | 'h3';
|
||||
};
|
||||
|
||||
export const Title = ({ headingLevel = 'h2' }: TitleSemanticsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
|
||||
@@ -16,7 +20,7 @@ export const Title = () => {
|
||||
>
|
||||
<Text
|
||||
$margin="none"
|
||||
as="h2"
|
||||
as={headingLevel}
|
||||
$color={colorsTokens['primary-text']}
|
||||
$zIndex={1}
|
||||
$size="1.375rem"
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function HomeBanner() {
|
||||
$gap={spacingsTokens['sm']}
|
||||
>
|
||||
<IconDocs
|
||||
aria-label={t('Docs Logo')}
|
||||
aria-label={t('Back to homepage')}
|
||||
width={64}
|
||||
color={colorsTokens['primary-text']}
|
||||
/>
|
||||
|
||||
@@ -62,6 +62,7 @@ export const HomeHeader = () => {
|
||||
$height="fit-content"
|
||||
>
|
||||
<IconDocs
|
||||
data-testid="header-icon-docs"
|
||||
aria-label={t('Docs Logo')}
|
||||
width={32}
|
||||
color={colorsTokens['primary-text']}
|
||||
|
||||
@@ -39,12 +39,17 @@ export const LanguagePicker = () => {
|
||||
<DropdownMenu
|
||||
options={optionsPicker}
|
||||
showArrow
|
||||
label={t('Select language')}
|
||||
buttonCss={css`
|
||||
&:hover {
|
||||
background-color: var(
|
||||
--c--components--button--primary-text--background--color-hover
|
||||
);
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--c--theme--colors--primary-500);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 0.6rem;
|
||||
& > div {
|
||||
|
||||
+33
-22
@@ -1,26 +1,24 @@
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, Icon, Text } from '@/components';
|
||||
import { Box, Icon, StyledLink, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { DocDefaultFilter } from '@/docs/doc-management';
|
||||
import { useLeftPanelStore } from '@/features/left-panel';
|
||||
|
||||
export const LeftPanelTargetFilters = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const { togglePanel } = useLeftPanelStore();
|
||||
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const target =
|
||||
(searchParams.get('target') as DocDefaultFilter) ??
|
||||
DocDefaultFilter.ALL_DOCS;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const defaultQueries = [
|
||||
{
|
||||
icon: 'apps',
|
||||
@@ -39,15 +37,20 @@ export const LeftPanelTargetFilters = () => {
|
||||
},
|
||||
];
|
||||
|
||||
const onSelectQuery = (query: DocDefaultFilter) => {
|
||||
const buildHref = (query: DocDefaultFilter) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set('target', query);
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
return `${pathname}?${params.toString()}`;
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
togglePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="nav"
|
||||
aria-label={t('Document sections')}
|
||||
$justify="center"
|
||||
$padding={{ horizontal: 'sm' }}
|
||||
$gap={spacingsTokens['2xs']}
|
||||
@@ -55,28 +58,36 @@ export const LeftPanelTargetFilters = () => {
|
||||
>
|
||||
{defaultQueries.map((query) => {
|
||||
const isActive = target === query.targetQuery;
|
||||
const href = buildHref(query.targetQuery);
|
||||
|
||||
return (
|
||||
<BoxButton
|
||||
aria-label={query.label}
|
||||
<StyledLink
|
||||
key={query.label}
|
||||
onClick={() => onSelectQuery(query.targetQuery)}
|
||||
$direction="row"
|
||||
aria-selected={isActive}
|
||||
$align="center"
|
||||
$justify="flex-start"
|
||||
$gap={spacingsTokens['xs']}
|
||||
$radius={spacingsTokens['3xs']}
|
||||
$padding={{ all: '2xs' }}
|
||||
href={href}
|
||||
aria-label={query.label}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
onClick={handleClick}
|
||||
$css={css`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: ${spacingsTokens['xs']};
|
||||
padding: ${spacingsTokens['2xs']};
|
||||
border-radius: ${spacingsTokens['3xs']};
|
||||
background-color: ${isActive
|
||||
? colorsTokens['greyscale-100']
|
||||
: undefined};
|
||||
font-weight: ${isActive ? 700 : undefined};
|
||||
: 'transparent'};
|
||||
font-weight: ${isActive ? 700 : 400};
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: ${colorsTokens['greyscale-100']};
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid ${colorsTokens['primary-500']};
|
||||
outline-offset: 2px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
@@ -86,7 +97,7 @@ export const LeftPanelTargetFilters = () => {
|
||||
<Text $variation={isActive ? '1000' : '700'} $size="sm">
|
||||
{query.label}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
</StyledLink>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
|
||||
+16
-3
@@ -1,4 +1,6 @@
|
||||
import { useModal } from '@openfun/cunningham-react';
|
||||
import { t } from 'i18next';
|
||||
import { DateTime } from 'luxon';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, StyledLink } from '@/components';
|
||||
@@ -14,11 +16,12 @@ type LeftPanelFavoriteItemProps = {
|
||||
|
||||
export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => {
|
||||
const shareModal = useModal();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="li"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
@@ -28,7 +31,8 @@ export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => {
|
||||
.pinned-actions {
|
||||
opacity: ${isDesktop ? 0 : 1};
|
||||
}
|
||||
&:hover {
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
cursor: pointer;
|
||||
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
@@ -36,11 +40,20 @@ export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid ${colorsTokens['primary-500']};
|
||||
outline-offset: 2px;
|
||||
border-radius: ${spacingsTokens['3xs']};
|
||||
}
|
||||
`}
|
||||
key={doc.id}
|
||||
className="--docs--left-panel-favorite-item"
|
||||
>
|
||||
<StyledLink href={`/docs/${doc.id}`} $css="overflow: auto;">
|
||||
<StyledLink
|
||||
href={`/docs/${doc.id}`}
|
||||
$css="overflow: auto;"
|
||||
aria-label={`${doc.title}, ${t('Updated')} ${DateTime.fromISO(doc.updated_at).toRelative()}`}
|
||||
>
|
||||
<SimpleDocItem showAccesses doc={doc} />
|
||||
</StyledLink>
|
||||
<div className="pinned-actions">
|
||||
|
||||
@@ -23,7 +23,11 @@ export const LeftPanelFavorites = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="--docs--left-panel-favorites">
|
||||
<Box
|
||||
as="nav"
|
||||
aria-label={t('Pinned documents')}
|
||||
className="--docs--left-panel-favorites"
|
||||
>
|
||||
<HorizontalSeparator $withPadding={false} />
|
||||
<Box
|
||||
$justify="center"
|
||||
@@ -41,6 +45,7 @@ export const LeftPanelFavorites = () => {
|
||||
{t('Pinned documents')}
|
||||
</Text>
|
||||
<InfiniteScroll
|
||||
as="ul"
|
||||
hasMore={docs.hasNextPage}
|
||||
isLoading={docs.isFetchingNextPage}
|
||||
next={() => void docs.fetchNextPage()}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { t } from 'i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { PropsWithChildren, useCallback, useState } from 'react';
|
||||
|
||||
@@ -53,20 +54,34 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
|
||||
>
|
||||
<Box $direction="row" $gap="2px">
|
||||
<Button
|
||||
data-testid="home-button"
|
||||
onClick={goToHome}
|
||||
aria-label={t('Back to homepage')}
|
||||
size="medium"
|
||||
color="tertiary-text"
|
||||
icon={
|
||||
<Icon $variation="800" $theme="primary" iconName="house" />
|
||||
<Icon
|
||||
$variation="800"
|
||||
$theme="primary"
|
||||
iconName="house"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{authenticated && (
|
||||
<Button
|
||||
data-testid="search-docs-button"
|
||||
onClick={openSearchModal}
|
||||
size="medium"
|
||||
color="tertiary-text"
|
||||
aria-label={t('Search docs')}
|
||||
icon={
|
||||
<Icon $variation="800" $theme="primary" iconName="search" />
|
||||
<Icon
|
||||
$variation="800"
|
||||
$theme="primary"
|
||||
iconName="search"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
+2
-1
@@ -19,9 +19,10 @@ export const LeftPanelHeaderButton = () => {
|
||||
});
|
||||
return (
|
||||
<Button
|
||||
data-testid="new-doc-button"
|
||||
color="primary"
|
||||
onClick={() => createDoc()}
|
||||
icon={<Icon $variation="000" iconName="add" />}
|
||||
icon={<Icon $variation="000" iconName="add" aria-hidden="true" />}
|
||||
disabled={isDocCreating}
|
||||
>
|
||||
{t('New doc')}
|
||||
|
||||
@@ -64,9 +64,7 @@ export class RequestSerializer {
|
||||
}
|
||||
|
||||
public static objectToArrayBuffer(ob: Record<string, unknown>) {
|
||||
return RequestSerializer.stringToArrayBuffer(
|
||||
JSON.stringify(ob),
|
||||
) as ArrayBuffer;
|
||||
return RequestSerializer.stringToArrayBuffer(JSON.stringify(ob));
|
||||
}
|
||||
|
||||
constructor(requestData: RequestData) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const fallbackLng = 'en';
|
||||
@@ -2,6 +2,7 @@ import i18next from 'i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import { fallbackLng } from './config';
|
||||
import resources from './translations.json';
|
||||
|
||||
// Add an initialization guard
|
||||
@@ -16,7 +17,7 @@ if (!isInitialized && !i18next.isInitialized) {
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: 'en',
|
||||
fallbackLng,
|
||||
debug: false,
|
||||
detection: {
|
||||
order: ['cookie', 'navigator'],
|
||||
@@ -35,6 +36,17 @@ if (!isInitialized && !i18next.isInitialized) {
|
||||
nsSeparator: false,
|
||||
keySeparator: false,
|
||||
})
|
||||
.then(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.setAttribute(
|
||||
'lang',
|
||||
i18next.language || fallbackLng,
|
||||
);
|
||||
i18next.on('languageChanged', (lang) => {
|
||||
document.documentElement.setAttribute('lang', lang);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => console.error('i18n initialization failed:', e));
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"Document accessible to any connected person": "Restr a c'hall bezañ tizhet gant ne vern piv a vefe kevreet",
|
||||
"Document duplicated successfully!": "Restr eilet gant berzh!",
|
||||
"Document owner": "Perc'henn ar restr",
|
||||
"Document sections": "Rannoù an teul",
|
||||
"Docx": "Docx",
|
||||
"Download": "Pellgargañ",
|
||||
"Download anyway": "Pellgargañ memestra",
|
||||
@@ -207,6 +208,7 @@
|
||||
"Doc visibility card": "Dokumenten-Sichtbarkeitskarte",
|
||||
"Docs": "Docs",
|
||||
"Docs Logo": "Docs-Logo",
|
||||
"Back to homepage": "Zurück zur Startseite",
|
||||
"Docs is already available, log in to use it now.": "Docs ist bereits verfügbar. Melden Sie sich an, um es jetzt zu nutzen.",
|
||||
"Docs makes real-time collaboration simple. Invite collaborators - public officials or external partners - with one click to see their changes live, while maintaining precise access control for data security.": "Docs macht die Zusammenarbeit in Echtzeit einfach. Laden Sie Mitarbeiter — Beamte oder externe Partner — mit einem Klick ein, um ihre Änderungen live zu sehen und dabei die genaue Zugangskontrolle zwecks Datensicherheit beizubehalten.",
|
||||
"Docs offers an intuitive writing experience. Its minimalist interface favors content over layout, while offering the essentials: media import, offline mode and keyboard shortcuts for greater efficiency.": "Docs bietet ein intuitives Schreiberlebnis. Seine minimalistische Oberfläche bevorzugt Inhalte über Layout, bietet aber das Wesentliche: Medien-Import, Offline-Modus und Tastaturkürzel für mehr Effizienz.",
|
||||
@@ -215,6 +217,7 @@
|
||||
"Document accessible to any connected person": "Dokument für jeden angemeldeten Benutzer zugänglich",
|
||||
"Document duplicated successfully!": "Dokument erfolgreich dupliziert!",
|
||||
"Document owner": "Besitzer des Dokuments",
|
||||
"Document sections": "Dokumentabschnitte",
|
||||
"Docx": "Docx",
|
||||
"Download": "Herunterladen",
|
||||
"Download anyway": "Trotzdem herunterladen",
|
||||
@@ -263,6 +266,7 @@
|
||||
"Modal confirmation to download the attachment": "Modale Bestätigung zum Herunterladen des Anhangs",
|
||||
"Modal confirmation to restore the version": "Modale Bestätigung um die Version wiederherzustellen",
|
||||
"More docs": "Weitere Dokumente",
|
||||
"More options": "Weitere Optionen",
|
||||
"Move": "Verschieben",
|
||||
"Move document": "Dokument verschieben",
|
||||
"Move to my docs": "In \"Meine Dokumente\" verschieben",
|
||||
@@ -306,6 +310,7 @@
|
||||
"Reset": "Zurücksetzen",
|
||||
"Restore": "Wiederherstellen",
|
||||
"Search": "Suchen",
|
||||
"Search docs": "Dokumente durchsuchen",
|
||||
"Search modal": "Suche Modal",
|
||||
"Search user result": "Suchergebnis",
|
||||
"Select a document": "Dokument auswählen",
|
||||
@@ -342,6 +347,7 @@
|
||||
"Unnamed document": "Unbenanntes Dokument",
|
||||
"Unpin": "Lösen",
|
||||
"Untitled document": "Unbenanntes Dokument",
|
||||
"Updated": "Aktualisiert",
|
||||
"Updated at": "Aktualisiert am",
|
||||
"Use as prompt": "Als Prompt verwenden",
|
||||
"Version history": "Versionsverlauf",
|
||||
@@ -357,6 +363,7 @@
|
||||
"Your access request for this document is pending.": "Ihre Zugriffsanfrage für dieses Dokument steht noch aus.",
|
||||
"Your current document will revert to this version.": "Ihr aktuelles Dokument wird auf diese Version zurückgesetzt.",
|
||||
"Your {{format}} was downloaded succesfully": "Ihr {{format}} wurde erfolgreich heruntergeladen",
|
||||
"Open the menu of actions for the document: {{title}}": "Öffnen Sie das Aktionsmenü für das Dokument: {{title}}",
|
||||
"home-content-open-source-part1": "Doms ist auf <2>Django Rest Framework</2> und <6>Next.js</6> aufgebaut. Wir verwenden auch <9>Yjs</9> und <13>BlockNote.js</13>, zwei Projekte, die wir mit Stolz sponsern.",
|
||||
"home-content-open-source-part2": "Sie können Docs ganz einfach selbst hosten (lesen Sie unsere <2>Installationsdokumentation</2>).<br/>Docs verwendet eine <7>Lizenz</7> (MIT), die auf Innovation und Unternehmen zugeschnitten ist.<br/>Beiträge sind willkommen (lesen Sie unsere Roadmap <13>hier</13>).\n",
|
||||
"home-content-open-source-part3": "Docs ist das Ergebnis einer gemeinsamen Anstrengung, die von der französischen Regierung 🇫🇷🥖 <1>(DINUM)</1> und der deutschen Regierung 🇩🇪🥨 <5>(ZenDiS)</5> geleitet wurde. “, „home-content-open-source-part3“: „Docs ist das Ergebnis einer gemeinsamen Anstrengung, die von der französischen Regierung 🇫🇷🥖 <1>(DINUM)</1> und der deutschen Regierung 🇩🇪🥨 <5>(ZenDiS)</5> geleitet wird."
|
||||
@@ -364,10 +371,17 @@
|
||||
},
|
||||
"en": {
|
||||
"translation": {
|
||||
"Back to homepage": "Back to Docs homepage",
|
||||
"Search docs": "Search docs",
|
||||
"More options": "More options",
|
||||
"Pinned documents": "Pinned documents",
|
||||
"Open actions menu for document: {{title}}": "Open actions menu for document: {{title}}",
|
||||
"Open the menu of actions for the document: {{title}}": "Open the menu of actions for the document: {{title}}",
|
||||
"Share with {{count}} users_one": "Share with {{count}} user",
|
||||
"Shared with {{count}} users_many": "Shared with {{count}} users",
|
||||
"Shared with {{count}} users_one": "Shared with {{count}} user",
|
||||
"Shared with {{count}} users_other": "Shared with {{count}} users"
|
||||
"Shared with {{count}} users_other": "Shared with {{count}} users",
|
||||
"Updated": "Updated"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
@@ -418,6 +432,7 @@
|
||||
"Doc visibility card": "Accesos al documento",
|
||||
"Docs": "Docs",
|
||||
"Docs Logo": "Logo de Docs",
|
||||
"Back to homepage": "Volver a la página de inicio",
|
||||
"Docs is already available, log in to use it now.": "Docs ya está disponible, inicia sesión para empezar a usarlo.",
|
||||
"Docs makes real-time collaboration simple. Invite collaborators - public officials or external partners - with one click to see their changes live, while maintaining precise access control for data security.": "Docs simplifica la colaboración en tiempo real. Invitar colaboradores - funcionarios públicos o socios externos - con un solo clic para ver sus cambios en vivo, manteniendo un control de acceso preciso para la seguridad de los datos.",
|
||||
"Docs offers an intuitive writing experience. Its minimalist interface favors content over layout, while offering the essentials: media import, offline mode and keyboard shortcuts for greater efficiency.": "Docs ofrece una experiencia de escritura intuitiva. Su interfaz minimalista favorece el contenido sobre el diseño, mientras ofrece lo esencial: importación de medios, modo sin conexión y atajos de teclado para una mayor eficiencia.",
|
||||
@@ -425,6 +440,7 @@
|
||||
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs: su nuevo compañero para colaborar en documentos de forma eficiente, intuitiva y segura.",
|
||||
"Document accessible to any connected person": "Documento accesible a cualquier persona conectada",
|
||||
"Document owner": "Propietario del documento",
|
||||
"Document sections": "Secciones del documento",
|
||||
"Docx": "Docx",
|
||||
"Download": "Descargar",
|
||||
"Download anyway": "Descargar de todos modos",
|
||||
@@ -466,6 +482,7 @@
|
||||
"Modal confirmation to download the attachment": "Modal de confirmación para descargar el archivo adjunto",
|
||||
"Modal confirmation to restore the version": "Modal de confirmación para restaurar la versión",
|
||||
"More docs": "Más documentos",
|
||||
"More options": "Más opciones",
|
||||
"My docs": "Mis documentos",
|
||||
"Name": "Nombre",
|
||||
"New doc": "Nuevo documento",
|
||||
@@ -478,6 +495,8 @@
|
||||
"Offline ?!": "¿¡Sin conexión!?",
|
||||
"Only invited people can access": "Solo las personas invitadas pueden acceder",
|
||||
"Open Source": "Código abierto",
|
||||
"Open actions menu for document: {{title}}": "Abrir menú de acciones para el documento: {{title}}",
|
||||
"Open the menu of actions for the document: {{title}}": "Abrir el menú de acciones para el documento: {{title}}",
|
||||
"Open the document options": "Abrir las opciones del documento",
|
||||
"Open the header menu": "Abrir el menú de encabezado",
|
||||
"Organize": "Organiza",
|
||||
@@ -502,6 +521,7 @@
|
||||
"Request access": "Solicitar acceso",
|
||||
"Restore": "Recuperar",
|
||||
"Search": "Buscar",
|
||||
"Search docs": "Buscar documentos",
|
||||
"Search modal": "Modal de búsqueda",
|
||||
"Search user result": "Resultado de la búsqueda de usuarios",
|
||||
"Select a document": "Selecciona un documento",
|
||||
@@ -534,6 +554,7 @@
|
||||
"Type the name of a document": "Escribe el nombre de un documento",
|
||||
"Unpin": "Desanclar",
|
||||
"Untitled document": "Documento sin título",
|
||||
"Updated": "Actualizado",
|
||||
"Updated at": "Actualizado a las",
|
||||
"Use as prompt": "Usar como prompt",
|
||||
"Version history": "Historial de versiones",
|
||||
@@ -608,6 +629,7 @@
|
||||
"Doc visibility card": "Carte de visibilité du doc",
|
||||
"Docs": "Docs",
|
||||
"Docs Logo": "Logo Docs",
|
||||
"Back to homepage": "Retour à la page d'accueil de Docs",
|
||||
"Docs is already available, log in to use it now.": "Docs est déjà disponible, connectez-vous pour l’utiliser dès maintenant.",
|
||||
"Docs makes real-time collaboration simple. Invite collaborators - public officials or external partners - with one click to see their changes live, while maintaining precise access control for data security.": "Docs simplifie la collaboration en temps réel. Invitez des collaborateurs - agents publics ou partenaires externes - d'un clic pour voir leurs modifications en direct, tout en gardant un contrôle précis des accès pour la sécurité des données.",
|
||||
"Docs offers an intuitive writing experience. Its minimalist interface favors content over layout, while offering the essentials: media import, offline mode and keyboard shortcuts for greater efficiency.": "Docs propose une expérience d'écriture intuitive. Son interface minimaliste privilégie le contenu sur la mise en page, tout en offrant l'essentiel : import de médias, mode hors-ligne et raccourcis clavier pour plus d'efficacité.",
|
||||
@@ -616,6 +638,7 @@
|
||||
"Document accessible to any connected person": "Document accessible à toute personne connectée",
|
||||
"Document duplicated successfully!": "Document dupliqué avec succès !",
|
||||
"Document owner": "Propriétaire du document",
|
||||
"Document sections": "Sections des documents",
|
||||
"Docx": "Docx",
|
||||
"Download": "Télécharger",
|
||||
"Download anyway": "Télécharger malgré tout",
|
||||
@@ -650,6 +673,8 @@
|
||||
"Image 403": "Image 403",
|
||||
"Insufficient access rights to view the document.": "Droits d'accès insuffisants pour voir le document.",
|
||||
"Invite": "Inviter",
|
||||
"Open actions menu for document: {{title}}": "Ouvrir le menu d'actions pour le document : {{title}}",
|
||||
"Open the menu of actions for the document: {{title}}": "Ouvrir le menu d'actions du document",
|
||||
"It is the card information about the document.": "Il s'agit de la carte d'information du document.",
|
||||
"It is the document title": "Il s'agit du titre du document",
|
||||
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Il semble que la page que vous cherchez n'existe pas ou ne puisse pas être affichée correctement.",
|
||||
@@ -673,6 +698,7 @@
|
||||
"Modal confirmation to download the attachment": "Modale de confirmation pour télécharger la pièce jointe",
|
||||
"Modal confirmation to restore the version": "Modale de confirmation pour restaurer la version",
|
||||
"More docs": "Plus de documents",
|
||||
"More options": "Plus d'options",
|
||||
"Move": "Déplacer",
|
||||
"Move document": "Déplacer le document",
|
||||
"Move to my docs": "Déplacer vers mes docs",
|
||||
@@ -719,6 +745,7 @@
|
||||
"Restore": "Restaurer",
|
||||
"Search": "Rechercher",
|
||||
"Search by title": "Recherchez par titre",
|
||||
"Search docs": "Rechercher un document",
|
||||
"Search modal": "Modale de partage",
|
||||
"Search user result": "Résultat de la recherche utilisateur",
|
||||
"Select a doc": "Sélectionnez un doc",
|
||||
@@ -756,6 +783,7 @@
|
||||
"Type a name or email": "Tapez un nom ou un email",
|
||||
"Type the name of a document": "Tapez le nom d'un document",
|
||||
"Unnamed document": "Document sans titre",
|
||||
"Updated": "Mise à jour",
|
||||
"Unpin": "Désépingler",
|
||||
"Untitled document": "Document sans titre",
|
||||
"Updated at": "Mise à jour le",
|
||||
@@ -974,6 +1002,7 @@
|
||||
"Doc visibility card": "Docs zichtbaarheid kaart",
|
||||
"Docs": "Docs",
|
||||
"Docs Logo": "Docs logo",
|
||||
"Back to homepage": "Terug naar Docs homepage",
|
||||
"Docs is already available, log in to use it now.": "Docs is beschikbaar, log in om het te gebruiken.",
|
||||
"Docs makes real-time collaboration simple. Invite collaborators - public officials or external partners - with one click to see their changes live, while maintaining precise access control for data security.": "Docs maakt real-time samenwerking eenvoudig. Nodig medewerkers - ambtenaren of externe partners - uit met één klik om hun veranderingen live te zien, terwijl de toegangscontrole voor de gegevensbeveiliging wordt gehandhaafd.",
|
||||
"Docs offers an intuitive writing experience. Its minimalist interface favors content over layout, while offering the essentials: media import, offline mode and keyboard shortcuts for greater efficiency.": "Docs biedt een intuïtieve schrijfervaring. De minimalistische interface geeft voorrang aan de inhoud boven de lay-out, terwijl essentiële zaken worden aangeboden: importeren van media, offline-modus en sneltoetsen voor grotere efficiëntie.",
|
||||
@@ -981,6 +1010,7 @@
|
||||
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs: Je nieuwe metgezel om efficiënt, intuïtief en veilig samen te werken aan documenten.",
|
||||
"Document accessible to any connected person": "Document is toegankelijk voor ieder verbonden persoon",
|
||||
"Document owner": "Document eigenaar",
|
||||
"Document sections": "Document secties",
|
||||
"Docx": "Docx",
|
||||
"Download": "Download",
|
||||
"Download anyway": "Download alsnog",
|
||||
@@ -1021,6 +1051,7 @@
|
||||
"Modal confirmation to download the attachment": "Venster bevestiging om bijlage te downloaden",
|
||||
"Modal confirmation to restore the version": "Bevestiging modal om de versie te herstellen",
|
||||
"More docs": "Meer documenten",
|
||||
"More options": "Meer opties",
|
||||
"My docs": "Mijn documenten",
|
||||
"Name": "Naam",
|
||||
"New doc": "Nieuw document",
|
||||
@@ -1033,6 +1064,8 @@
|
||||
"Offline ?!": "Offline ?!",
|
||||
"Only invited people can access": "Alleen uitgenodigde gebruikers hebben toegang",
|
||||
"Open Source": "Open Source",
|
||||
"Open actions menu for document: {{title}}": "Open actiemenu voor document: {{title}}",
|
||||
"Open the menu of actions for the document: {{title}}": "Open het menu van acties voor het document: {{title}}",
|
||||
"Open the document options": "Open document opties",
|
||||
"Open the header menu": "Open het hoofdmenu",
|
||||
"Organize": "Organiseer",
|
||||
@@ -1055,6 +1088,7 @@
|
||||
"Rephrase": "Herschrijf",
|
||||
"Restore": "Herstel",
|
||||
"Search": "Zoeken",
|
||||
"Search docs": "Documenten zoeken",
|
||||
"Search modal": "Zoek modal",
|
||||
"Search user result": "Zoek resultaat",
|
||||
"Select a document": "Selecteer een document",
|
||||
@@ -1087,6 +1121,7 @@
|
||||
"Type the name of a document": "Vul de naam van een document in",
|
||||
"Unpin": "Losmaken",
|
||||
"Untitled document": "Naamloos document",
|
||||
"Updated": "Bijgewerkt",
|
||||
"Updated at": "Geüpdate op",
|
||||
"Use as prompt": "Gebruik als prompt",
|
||||
"Version history": "Versie historie",
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
import { Head, Html, Main, NextScript } from 'next/document';
|
||||
import Document, {
|
||||
DocumentContext,
|
||||
Head,
|
||||
Html,
|
||||
Main,
|
||||
NextScript,
|
||||
} from 'next/document';
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<body suppressHydrationWarning={process.env.NODE_ENV === 'development'}>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
import { fallbackLng } from '../i18n/config';
|
||||
|
||||
class MyDocument extends Document<{ locale: string }> {
|
||||
static async getInitialProps(ctx: DocumentContext) {
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
return {
|
||||
...initialProps,
|
||||
locale: ctx.locale || fallbackLng,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Html lang={this.props.locale}>
|
||||
<Head />
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument;
|
||||
|
||||
@@ -52,7 +52,7 @@ main ::-webkit-scrollbar-thumb:hover,
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
overflow-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"APP_IMPRESS": "yarn workspace app-impress",
|
||||
"APP_E2E": "yarn workspace app-e2e",
|
||||
"I18N": "yarn workspace packages-i18n",
|
||||
"COMMON": "yarn workspace package-docs",
|
||||
"COLLABORATION_SERVER": "yarn workspace server-y-provider",
|
||||
"app:dev": "yarn APP_IMPRESS run dev",
|
||||
"app:start": "yarn APP_IMPRESS run start",
|
||||
@@ -20,7 +21,7 @@
|
||||
"app:test": "yarn APP_IMPRESS run test",
|
||||
"ci:build": "yarn APP_IMPRESS run build:ci",
|
||||
"e2e:test": "yarn APP_E2E run test",
|
||||
"lint": "yarn APP_IMPRESS run lint && yarn APP_E2E run lint && yarn workspace eslint-config-impress run lint && yarn I18N run lint && yarn COLLABORATION_SERVER run lint",
|
||||
"lint": "yarn APP_IMPRESS run lint && yarn APP_E2E run lint && yarn workspace eslint-config-impress run lint && yarn I18N run lint && yarn COMMON run lint && yarn COLLABORATION_SERVER run lint",
|
||||
"i18n:extract": "yarn I18N run extract-translation",
|
||||
"i18n:deploy": "yarn I18N run format-deploy && yarn APP_IMPRESS prettier",
|
||||
"i18n:test": "yarn I18N run test",
|
||||
@@ -28,15 +29,15 @@
|
||||
"server:test": "yarn COLLABORATION_SERVER run test"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "22.16.5",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@typescript-eslint/eslint-plugin": "8.38.0",
|
||||
"@typescript-eslint/parser": "8.38.0",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/react": "19.1.9",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"@typescript-eslint/eslint-plugin": "8.39.0",
|
||||
"@typescript-eslint/parser": "8.39.0",
|
||||
"eslint": "8.57.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"typescript": "5.8.3",
|
||||
"typescript": "5.9.2",
|
||||
"yjs": "13.6.27"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['impress/next'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
rules: {
|
||||
'@next/next/no-html-link-for-pages': 'off',
|
||||
},
|
||||
ignorePatterns: ['node_modules'],
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "package-docs",
|
||||
"version": "3.5.0",
|
||||
"license": "MIT",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .ts,.tsx src/"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@blocknote/core": "0.35.0",
|
||||
"@blocknote/react": "0.35.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-config-impress": "*",
|
||||
"typescript": "*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { BlockNoteEditor, insertOrUpdateBlock } from '@blocknote/core';
|
||||
import { createReactBlockSpec } from '@blocknote/react';
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
interface DividerBlockProps {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export const createDividerBlock = ({ color }: DividerBlockProps) => {
|
||||
return createReactBlockSpec(
|
||||
{
|
||||
type: 'divider',
|
||||
propSchema: {},
|
||||
content: 'none',
|
||||
},
|
||||
{
|
||||
render: () => {
|
||||
return (
|
||||
<hr
|
||||
style={{
|
||||
width: '100%',
|
||||
background: color,
|
||||
margin: '1rem 0',
|
||||
border: `1px solid ${color}`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const getDividerReactSlashMenuItems = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
editor: BlockNoteEditor<any, any, any>,
|
||||
t: TFunction<'translation', undefined>,
|
||||
group: string,
|
||||
icon: React.ReactNode,
|
||||
) => [
|
||||
{
|
||||
title: t('Divider'),
|
||||
onItemClick: () => {
|
||||
insertOrUpdateBlock(editor, {
|
||||
type: 'divider',
|
||||
});
|
||||
},
|
||||
aliases: ['divider', 'hr', 'horizontal rule', 'line', 'separator'],
|
||||
group,
|
||||
icon,
|
||||
subtext: t('Add a horizontal line'),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
export * from './DividerBlock';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './custom-blocks/';
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -6,19 +6,19 @@
|
||||
"lint": "eslint --ext .js ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "15.4.4",
|
||||
"@tanstack/eslint-plugin-query": "5.81.2",
|
||||
"@next/eslint-plugin-next": "15.4.6",
|
||||
"@tanstack/eslint-plugin-query": "5.83.1",
|
||||
"@typescript-eslint/eslint-plugin": "*",
|
||||
"@typescript-eslint/parser": "*",
|
||||
"eslint": "*",
|
||||
"eslint-config-next": "15.4.4",
|
||||
"eslint-config-next": "15.4.6",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-jest": "29.0.1",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-playwright": "2.2.0",
|
||||
"eslint-plugin-prettier": "5.5.3",
|
||||
"eslint-plugin-testing-library": "7.6.1",
|
||||
"eslint-plugin-playwright": "2.2.2",
|
||||
"eslint-plugin-prettier": "5.5.4",
|
||||
"eslint-plugin-testing-library": "7.6.3",
|
||||
"prettier": "3.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"i18next-parser": "9.3.0",
|
||||
"jest": "30.0.5",
|
||||
"ts-jest": "29.4.0",
|
||||
"ts-jest": "29.4.1",
|
||||
"typescript": "*",
|
||||
"yargs": "18.0.0"
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
"dependencies": {
|
||||
"@blocknote/server-util": "0.35.0",
|
||||
"@hocuspocus/server": "2.15.2",
|
||||
"@sentry/node": "9.42.0",
|
||||
"@sentry/profiling-node": "9.42.0",
|
||||
"@sentry/node": "10.2.0",
|
||||
"@sentry/profiling-node": "10.2.0",
|
||||
"axios": "1.11.0",
|
||||
"cors": "2.8.5",
|
||||
"express": "5.1.0",
|
||||
@@ -36,7 +36,7 @@
|
||||
"@types/node": "*",
|
||||
"@types/supertest": "6.0.3",
|
||||
"@types/ws": "8.18.1",
|
||||
"cross-env": "7.0.3",
|
||||
"cross-env": "10.0.0",
|
||||
"eslint-config-impress": "*",
|
||||
"nodemon": "3.1.10",
|
||||
"supertest": "7.1.4",
|
||||
|
||||
+1735
-3622
File diff suppressed because it is too large
Load Diff
@@ -50,9 +50,6 @@ backend:
|
||||
DB_USER: dinum
|
||||
DB_PASSWORD: pass
|
||||
DB_PORT: 5432
|
||||
POSTGRES_DB: impress
|
||||
POSTGRES_USER: dinum
|
||||
POSTGRES_PASSWORD: pass
|
||||
REDIS_URL: redis://default:pass@redis-master:6379/1
|
||||
DJANGO_CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
|
||||
AWS_S3_ENDPOINT_URL: http://minio.impress.svc.cluster.local:9000
|
||||
|
||||
Reference in New Issue
Block a user