mirror of
https://github.com/suitenumerique/docs.git
synced 2026-05-06 07:02:03 +02:00
Compare commits
26 Commits
refacto/re
...
new-ui/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37e78b542f | ||
|
|
e9a66dd1b3 | ||
|
|
572a26d1bb | ||
|
|
1260310199 | ||
|
|
9e900f5b44 | ||
|
|
5363a1328e | ||
|
|
33986a6bef | ||
|
|
b8c0c40693 | ||
|
|
bdcb337d0d | ||
|
|
fb22e24a1a | ||
|
|
c21ef3e85c | ||
|
|
194cf2b90f | ||
|
|
8524f59deb | ||
|
|
d92a8c6260 | ||
|
|
94a65da8ca | ||
|
|
c782b05594 | ||
|
|
25e76a76b7 | ||
|
|
2ab911426c | ||
|
|
1049033917 | ||
|
|
de7a601d8b | ||
|
|
b361700638 | ||
|
|
5295abade8 | ||
|
|
4fa7a367a9 | ||
|
|
b00dedf74e | ||
|
|
2bed0cab93 | ||
|
|
b8d8f1830e |
1
.github/workflows/docker-hub.yml
vendored
1
.github/workflows/docker-hub.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'main-new-ui'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -9,9 +9,22 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## Added
|
||||
- ✨(frontend) WIP: New ui
|
||||
- 💄(frontend) Add left panel #420
|
||||
- 💄(frontend) updating the header and leftpanel for responsive #421
|
||||
- 💄(frontend) update DocsGrid component #431
|
||||
- 💄(frontend) update DocsGridOptions component #432
|
||||
- 💄(frontend) update DocHeader ui #446
|
||||
- 💄(frontend) update doc versioning ui #463
|
||||
- 💄(frontend) update doc summary ui #473
|
||||
- 🐛(frontend) fix doc grid button #478
|
||||
- 💄(frontend) update doc home left panel #475
|
||||
- 🐛(frontend) update doc editor height #481
|
||||
|
||||
## [1.8.2] - 2024-11-28
|
||||
|
||||
|
||||
## Changed
|
||||
|
||||
- ♻️(SW) change strategy html caching #460
|
||||
@@ -33,12 +46,15 @@ and this project adheres to
|
||||
- 🌐(backend) add German translation #259
|
||||
- 🌐(frontend) add German translation #255
|
||||
- ✨(frontend) add a broadcast store #387
|
||||
- ✨(backend) config endpoint #425
|
||||
- 💄(frontend) update DocsGrid component #431
|
||||
- ✨(backend) whitelist pod's IP address #443
|
||||
- ✨(backend) config endpoint #425
|
||||
- ✨(frontend) config endpoint #424
|
||||
- ✨(frontend) add sentry #424
|
||||
- ✨(frontend) add crisp chatbot #450
|
||||
|
||||
|
||||
## Changed
|
||||
|
||||
- ⚡️(backend) optimize number of queries on document list view #411
|
||||
|
||||
@@ -47,7 +47,7 @@ dependencies = [
|
||||
"jsonschema==4.23.0",
|
||||
"markdown==3.7",
|
||||
"nested-multipart-parser==1.5.0",
|
||||
"openai==1.52.0",
|
||||
"openai==1.55.3",
|
||||
"psycopg[binary]==3.2.3",
|
||||
"PyJWT==2.9.0",
|
||||
"pypandoc==1.14",
|
||||
|
||||
@@ -36,18 +36,25 @@ export const createDoc = async (
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Create a new document',
|
||||
name: 'New doc',
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.getByRole('heading', { name: 'Untitled document' }).click();
|
||||
await page.keyboard.type(randomDocs[i]);
|
||||
await page.getByText('Created at ').click();
|
||||
const input = page.getByRole('textbox', { name: 'doc title input' });
|
||||
await input.click();
|
||||
await input.fill(randomDocs[i]);
|
||||
await input.blur();
|
||||
}
|
||||
|
||||
return randomDocs;
|
||||
};
|
||||
|
||||
export const verifyDocName = async (page: Page, docName: string) => {
|
||||
const input = page.getByRole('textbox', { name: 'doc title input' });
|
||||
await expect(input).toBeVisible();
|
||||
await expect(input).toHaveText(docName);
|
||||
};
|
||||
|
||||
export const addNewMember = async (
|
||||
page: Page,
|
||||
index: number,
|
||||
@@ -97,24 +104,22 @@ export const goToGridDoc = async (
|
||||
const header = page.locator('header').first();
|
||||
await header.locator('h2').getByText('Docs').click();
|
||||
|
||||
const datagrid = page.getByLabel('Datagrid of the documents page 1');
|
||||
const datagridTable = datagrid.getByRole('table');
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid).toBeVisible();
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeHidden();
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
const rows = datagridTable.getByRole('row');
|
||||
const rows = docsGrid.getByRole('row');
|
||||
expect(await rows.count()).toEqual(20);
|
||||
const row = title
|
||||
? rows.filter({
|
||||
hasText: title,
|
||||
})
|
||||
: rows.nth(nthRow);
|
||||
|
||||
const docTitleCell = row.getByRole('cell').nth(1);
|
||||
|
||||
const docTitle = await docTitleCell.textContent();
|
||||
await expect(row).toBeVisible();
|
||||
|
||||
const docTitleContent = row.locator('[aria-describedby="doc-title"]').first();
|
||||
const docTitle = await docTitleContent.textContent();
|
||||
expect(docTitle).toBeDefined();
|
||||
|
||||
await row.getByRole('link').first().click();
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from 'path';
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc } from './common';
|
||||
import { createDoc, verifyDocName } from './common';
|
||||
|
||||
const config = {
|
||||
CRISP_WEBSITE_ID: null,
|
||||
@@ -128,7 +128,8 @@ test.describe('Config', () => {
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
await expect(page.locator('h2').getByText(randomDoc[0])).toBeVisible();
|
||||
|
||||
await verifyDocName(page, randomDoc[0]);
|
||||
|
||||
const webSocket = await webSocketPromise;
|
||||
expect(webSocket.url()).toContain('ws://localhost:4444/');
|
||||
|
||||
@@ -18,14 +18,11 @@ test.describe('Doc Create', () => {
|
||||
const header = page.locator('header').first();
|
||||
await header.locator('h2').getByText('Docs').click();
|
||||
|
||||
const datagrid = page.getByLabel('Datagrid of the documents page 1');
|
||||
const datagridTable = datagrid.getByRole('table');
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeVisible();
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(datagridTable.getByText(docTitle)).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid).toBeVisible();
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeHidden();
|
||||
await expect(docsGrid.getByText(docTitle)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,12 @@ import path from 'path';
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, goToGridDoc, mockedDocument } from './common';
|
||||
import {
|
||||
createDoc,
|
||||
goToGridDoc,
|
||||
mockedDocument,
|
||||
verifyDocName,
|
||||
} from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -90,7 +95,7 @@ test.describe('Doc Editor', () => {
|
||||
});
|
||||
|
||||
const randomDoc = await createDoc(page, 'doc-editor', browserName, 1);
|
||||
await expect(page.locator('h2').getByText(randomDoc[0])).toBeVisible();
|
||||
await verifyDocName(page, randomDoc[0]);
|
||||
|
||||
const webSocket = await webSocketPromise;
|
||||
expect(webSocket.url()).toContain('ws://localhost:4444/');
|
||||
@@ -110,7 +115,7 @@ test.describe('Doc Editor', () => {
|
||||
}) => {
|
||||
const randomDoc = await createDoc(page, 'doc-markdown', browserName, 1);
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDoc[0])).toBeVisible();
|
||||
await verifyDocName(page, randomDoc[0]);
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
await editor.click();
|
||||
@@ -135,7 +140,7 @@ test.describe('Doc Editor', () => {
|
||||
}) => {
|
||||
// Check the first doc
|
||||
const [firstDoc] = await createDoc(page, 'doc-switch-1', browserName, 1);
|
||||
await expect(page.locator('h2').getByText(firstDoc)).toBeVisible();
|
||||
await verifyDocName(page, firstDoc);
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
await editor.click();
|
||||
@@ -144,7 +149,8 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
// Check the second doc
|
||||
const [secondDoc] = await createDoc(page, 'doc-switch-2', browserName, 1);
|
||||
await expect(page.locator('h2').getByText(secondDoc)).toBeVisible();
|
||||
await verifyDocName(page, secondDoc);
|
||||
|
||||
await expect(editor.getByText('Hello World Doc 1')).toBeHidden();
|
||||
await editor.click();
|
||||
await editor.fill('Hello World Doc 2');
|
||||
@@ -154,7 +160,7 @@ test.describe('Doc Editor', () => {
|
||||
await goToGridDoc(page, {
|
||||
title: firstDoc,
|
||||
});
|
||||
await expect(page.locator('h2').getByText(firstDoc)).toBeVisible();
|
||||
await verifyDocName(page, firstDoc);
|
||||
await expect(editor.getByText('Hello World Doc 2')).toBeHidden();
|
||||
await expect(editor.getByText('Hello World Doc 1')).toBeVisible();
|
||||
});
|
||||
@@ -165,7 +171,7 @@ test.describe('Doc Editor', () => {
|
||||
}) => {
|
||||
// Check the first doc
|
||||
const [doc] = await createDoc(page, 'doc-saves-change', browserName, 1);
|
||||
await expect(page.locator('h2').getByText(doc)).toBeVisible();
|
||||
await verifyDocName(page, doc);
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
await editor.click();
|
||||
@@ -176,7 +182,7 @@ test.describe('Doc Editor', () => {
|
||||
nthRow: 2,
|
||||
});
|
||||
|
||||
await expect(page.locator('h2').getByText(secondDoc)).toBeVisible();
|
||||
await verifyDocName(page, secondDoc);
|
||||
|
||||
await goToGridDoc(page, {
|
||||
title: doc,
|
||||
@@ -191,7 +197,8 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
// Check the first doc
|
||||
const doc = await goToGridDoc(page);
|
||||
await expect(page.locator('h2').getByText(doc)).toBeVisible();
|
||||
|
||||
await verifyDocName(page, doc);
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
await editor.click();
|
||||
|
||||
@@ -3,13 +3,44 @@ import cs from 'convert-stream';
|
||||
import jsdom from 'jsdom';
|
||||
import pdf from 'pdf-parse';
|
||||
|
||||
import { createDoc } from './common';
|
||||
import { createDoc, verifyDocName } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Doc Export', () => {
|
||||
test('it check if all elements are visible', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await createDoc(page, 'doc-editor', browserName, 1);
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'download',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Download$/ })
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(
|
||||
'Upload your docs to a Microsoft Word, Open Office or PDF document',
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('combobox', { name: 'Template' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('combobox', { name: 'Format' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Close the modal' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Download' })).toBeVisible();
|
||||
});
|
||||
test('it converts the doc to pdf with a template integrated', async ({
|
||||
page,
|
||||
browserName,
|
||||
@@ -20,15 +51,14 @@ test.describe('Doc Export', () => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
});
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export',
|
||||
name: 'download',
|
||||
})
|
||||
.click();
|
||||
|
||||
@@ -57,19 +87,19 @@ test.describe('Doc Export', () => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.docx`);
|
||||
});
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export',
|
||||
name: 'download',
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.getByText('Docx').click();
|
||||
await page.getByRole('combobox', { name: 'Format' }).click();
|
||||
await page.getByRole('option', { name: 'Word / Open Office' }).click();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
@@ -97,7 +127,7 @@ test.describe('Doc Export', () => {
|
||||
await route.continue();
|
||||
});
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
await page.locator('.bn-block-outer').last().fill('Hello World');
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
@@ -190,10 +220,9 @@ test.describe('Doc Export', () => {
|
||||
.click();
|
||||
|
||||
// Download
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export',
|
||||
name: 'download',
|
||||
})
|
||||
.click();
|
||||
|
||||
|
||||
@@ -1,264 +1,14 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
type SmallDoc = {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Documents Grid', () => {
|
||||
test('checks all the elements are visible', async ({ page }) => {
|
||||
await expect(page.locator('h2').getByText('Documents')).toBeVisible();
|
||||
|
||||
const datagrid = page
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
.getByRole('table');
|
||||
|
||||
const thead = datagrid.locator('thead');
|
||||
await expect(thead.getByText(/Document name/i)).toBeVisible();
|
||||
await expect(thead.getByText(/Created at/i)).toBeVisible();
|
||||
await expect(thead.getByText(/Updated at/i)).toBeVisible();
|
||||
await expect(thead.getByText(/Your role/i)).toBeVisible();
|
||||
await expect(thead.getByText(/Members/i)).toBeVisible();
|
||||
|
||||
const row1 = datagrid.getByRole('row').nth(1).getByRole('cell');
|
||||
const docName = await row1.nth(1).textContent();
|
||||
expect(docName).toBeDefined();
|
||||
|
||||
const docCreatedAt = await row1.nth(2).textContent();
|
||||
expect(docCreatedAt).toBeDefined();
|
||||
|
||||
const docUpdatedAt = await row1.nth(3).textContent();
|
||||
expect(docUpdatedAt).toBeDefined();
|
||||
|
||||
const docRole = await row1.nth(4).textContent();
|
||||
expect(
|
||||
docRole &&
|
||||
['Administrator', 'Owner', 'Reader', 'Editor'].includes(docRole),
|
||||
).toBeTruthy();
|
||||
|
||||
const docUserNumber = await row1.nth(5).textContent();
|
||||
expect(docUserNumber).toBeDefined();
|
||||
|
||||
// Open the document
|
||||
await row1.nth(1).click();
|
||||
|
||||
await expect(page.locator('h2').getByText(docName!)).toBeVisible();
|
||||
});
|
||||
|
||||
[
|
||||
{
|
||||
nameColumn: 'Document name',
|
||||
ordering: 'title',
|
||||
cellNumber: 1,
|
||||
orderDefault: '',
|
||||
orderDesc: '&ordering=-title',
|
||||
orderAsc: '&ordering=title',
|
||||
defaultColumn: false,
|
||||
},
|
||||
{
|
||||
nameColumn: 'Created at',
|
||||
ordering: 'created_at',
|
||||
cellNumber: 2,
|
||||
orderDefault: '',
|
||||
orderDesc: '&ordering=-created_at',
|
||||
orderAsc: '&ordering=created_at',
|
||||
defaultColumn: false,
|
||||
},
|
||||
{
|
||||
nameColumn: 'Updated at',
|
||||
ordering: 'updated_at',
|
||||
cellNumber: 3,
|
||||
orderDefault: '&ordering=-updated_at',
|
||||
orderDesc: '&ordering=updated_at',
|
||||
orderAsc: '',
|
||||
defaultColumn: true,
|
||||
},
|
||||
].forEach(
|
||||
({
|
||||
nameColumn,
|
||||
ordering,
|
||||
cellNumber,
|
||||
orderDefault,
|
||||
orderDesc,
|
||||
orderAsc,
|
||||
defaultColumn,
|
||||
}) => {
|
||||
test(`checks datagrid ordering ${ordering}`, async ({ page }) => {
|
||||
const responsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/documents/?page=1${orderDefault}`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
const responsePromiseOrderingDesc = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/documents/?page=1${orderDesc}`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
const responsePromiseOrderingAsc = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/documents/?page=1${orderAsc}`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
// Checks the initial state
|
||||
const datagrid = page.getByLabel('Datagrid of the documents page 1');
|
||||
const datagridTable = datagrid.getByRole('table');
|
||||
const thead = datagridTable.locator('thead');
|
||||
|
||||
const response = await responsePromise;
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
const docNameRow1 = datagridTable
|
||||
.getByRole('row')
|
||||
.nth(1)
|
||||
.getByRole('cell')
|
||||
.nth(cellNumber);
|
||||
const docNameRow2 = datagridTable
|
||||
.getByRole('row')
|
||||
.nth(2)
|
||||
.getByRole('cell')
|
||||
.nth(cellNumber);
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Initial state
|
||||
await expect(docNameRow1).toHaveText(/.*/);
|
||||
await expect(docNameRow2).toHaveText(/.*/);
|
||||
const initialDocNameRow1 = await docNameRow1.textContent();
|
||||
const initialDocNameRow2 = await docNameRow2.textContent();
|
||||
|
||||
expect(initialDocNameRow1).toBeDefined();
|
||||
expect(initialDocNameRow2).toBeDefined();
|
||||
|
||||
// Ordering ASC
|
||||
await thead.getByText(nameColumn).click();
|
||||
|
||||
const responseOrderingAsc = await responsePromiseOrderingAsc;
|
||||
expect(responseOrderingAsc.ok()).toBeTruthy();
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
await expect(docNameRow1).toHaveText(/.*/);
|
||||
await expect(docNameRow2).toHaveText(/.*/);
|
||||
const textDocNameRow1Asc = await docNameRow1.textContent();
|
||||
const textDocNameRow2Asc = await docNameRow2.textContent();
|
||||
|
||||
const compare = (comp1: string, comp2: string) => {
|
||||
const comparisonResult = comp1.localeCompare(comp2, 'en', {
|
||||
caseFirst: 'false',
|
||||
ignorePunctuation: true,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
return defaultColumn ? comparisonResult >= 0 : comparisonResult <= 0;
|
||||
};
|
||||
|
||||
expect(
|
||||
textDocNameRow1Asc &&
|
||||
textDocNameRow2Asc &&
|
||||
compare(textDocNameRow1Asc, textDocNameRow2Asc),
|
||||
).toBeTruthy();
|
||||
|
||||
// Ordering Desc
|
||||
await thead.getByText(nameColumn).click();
|
||||
|
||||
const responseOrderingDesc = await responsePromiseOrderingDesc;
|
||||
expect(responseOrderingDesc.ok()).toBeTruthy();
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
await expect(docNameRow1).toHaveText(/.*/);
|
||||
await expect(docNameRow2).toHaveText(/.*/);
|
||||
const textDocNameRow1Desc = await docNameRow1.textContent();
|
||||
const textDocNameRow2Desc = await docNameRow2.textContent();
|
||||
|
||||
expect(
|
||||
textDocNameRow1Desc &&
|
||||
textDocNameRow2Desc &&
|
||||
compare(textDocNameRow2Desc, textDocNameRow1Desc),
|
||||
).toBeTruthy();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test('checks the pagination', async ({ page }) => {
|
||||
const responsePromisePage1 = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/documents/?page=1`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
const responsePromisePage2 = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/documents/?page=2`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
const datagridPage1 = page
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
.getByRole('table');
|
||||
|
||||
const responsePage1 = await responsePromisePage1;
|
||||
expect(responsePage1.ok()).toBeTruthy();
|
||||
|
||||
await expect(
|
||||
datagridPage1.getByRole('row').nth(1).getByRole('cell').nth(1),
|
||||
).toHaveText(/.*/);
|
||||
|
||||
await page.getByLabel('Go to page 2').click();
|
||||
|
||||
const datagridPage2 = page
|
||||
.getByLabel('Datagrid of the documents page 2')
|
||||
.getByRole('table');
|
||||
|
||||
const responsePage2 = await responsePromisePage2;
|
||||
expect(responsePage2.ok()).toBeTruthy();
|
||||
|
||||
await expect(
|
||||
datagridPage2.getByRole('row').nth(1).getByRole('cell').nth(1),
|
||||
).toHaveText(/.*/);
|
||||
});
|
||||
|
||||
test('it deletes the document', async ({ page }) => {
|
||||
const datagrid = page
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
.getByRole('table');
|
||||
|
||||
const docRow = datagrid.getByRole('row').nth(1).getByRole('cell');
|
||||
|
||||
const docName = await docRow.nth(1).textContent();
|
||||
|
||||
await docRow
|
||||
.getByRole('button', {
|
||||
name: 'Delete the document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.locator('h2').getByText(`Deleting the document "${docName}"`),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Confirm deletion',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document has been deleted.'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(datagrid.getByText(docName!)).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Documents Grid mobile', () => {
|
||||
test.use({ viewport: { width: 500, height: 1200 } });
|
||||
|
||||
@@ -326,19 +76,257 @@ test.describe('Documents Grid mobile', () => {
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const datagrid = page.getByLabel('Datagrid of the documents page 1');
|
||||
const tableDatagrid = datagrid.getByRole('table');
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid).toBeVisible();
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeHidden();
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
const rows = tableDatagrid.getByRole('row');
|
||||
const rows = docsGrid.getByRole('row');
|
||||
const row = rows.filter({
|
||||
hasText: 'My mocked document',
|
||||
});
|
||||
|
||||
await expect(row.getByRole('cell').nth(0)).toHaveText('My mocked document');
|
||||
await expect(row.getByRole('cell').nth(1)).toHaveText('Public');
|
||||
await expect(
|
||||
row.locator('[aria-describedby="doc-title"]').nth(0),
|
||||
).toHaveText('My mocked document');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Document grid item options', () => {
|
||||
test('it deletes the document', async ({ page }) => {
|
||||
let docs: SmallDoc[] = [];
|
||||
const response = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('documents/?page=1') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
const result = await response.json();
|
||||
docs = result.results as SmallDoc[];
|
||||
|
||||
const button = page.getByTestId(`docs-grid-actions-button-${docs[0].id}`);
|
||||
await expect(button).toBeVisible();
|
||||
await button.click();
|
||||
|
||||
const removeButton = page.getByTestId(
|
||||
`docs-grid-actions-remove-${docs[0].id}`,
|
||||
);
|
||||
await expect(removeButton).toBeVisible();
|
||||
await removeButton.click();
|
||||
|
||||
await expect(
|
||||
page.locator('h2').getByText(`Deleting the document "${docs[0].title}"`),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Confirm deletion',
|
||||
})
|
||||
.click();
|
||||
|
||||
const refetchResponse = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('documents/?page=1') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
const resultRefetch = await refetchResponse.json();
|
||||
expect(resultRefetch.count).toBe(result.count - 1);
|
||||
await expect(page.getByTestId('main-layout-loader')).toBeHidden();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document has been deleted.'),
|
||||
).toBeVisible();
|
||||
await expect(button).toBeHidden();
|
||||
});
|
||||
|
||||
test("it checks if the delete option is disabled if we don't have the destroy capability", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route('*/**/api/v1.0/documents/?page=1', async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
results: [
|
||||
{
|
||||
id: 'mocked-document-id',
|
||||
content: '',
|
||||
title: 'Mocked document',
|
||||
accesses: [],
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
link_configuration: false,
|
||||
versions_destroy: false,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
accesses_manage: false, // Means not admin
|
||||
update: false,
|
||||
partial_update: false, // Means not editor
|
||||
retrieve: true,
|
||||
},
|
||||
link_reach: 'restricted',
|
||||
created_at: '2021-09-01T09:00:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
await page.goto('/');
|
||||
|
||||
const button = page.getByTestId(
|
||||
`docs-grid-actions-button-mocked-document-id`,
|
||||
);
|
||||
await expect(button).toBeVisible();
|
||||
await button.click();
|
||||
const removeButton = page.getByTestId(
|
||||
`docs-grid-actions-remove-mocked-document-id`,
|
||||
);
|
||||
await expect(removeButton).toBeVisible();
|
||||
await removeButton.isDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Documents filters', () => {
|
||||
test('it checks the prebuild left panel filters', async ({ page }) => {
|
||||
// All Docs
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeVisible();
|
||||
const response = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('documents/?page=1') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
const result = await response.json();
|
||||
const allCount = result.count as number;
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeHidden();
|
||||
|
||||
const allDocs = page.getByLabel('All docs');
|
||||
const myDocs = page.getByLabel('My docs');
|
||||
const sharedWithMe = page.getByLabel('Shared with me');
|
||||
|
||||
// Initial state
|
||||
await expect(allDocs).toBeVisible();
|
||||
await expect(allDocs).toHaveCSS('background-color', 'rgb(238, 238, 238)');
|
||||
await expect(allDocs).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
await expect(myDocs).toBeVisible();
|
||||
await expect(myDocs).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
||||
await expect(myDocs).toHaveAttribute('aria-selected', 'false');
|
||||
|
||||
await expect(sharedWithMe).toBeVisible();
|
||||
await expect(sharedWithMe).toHaveCSS(
|
||||
'background-color',
|
||||
'rgba(0, 0, 0, 0)',
|
||||
);
|
||||
await expect(sharedWithMe).toHaveAttribute('aria-selected', 'false');
|
||||
|
||||
await allDocs.click();
|
||||
|
||||
let url = new URL(page.url());
|
||||
let target = url.searchParams.get('target');
|
||||
expect(target).toBe('all_docs');
|
||||
|
||||
// My docs
|
||||
await myDocs.click();
|
||||
url = new URL(page.url());
|
||||
target = url.searchParams.get('target');
|
||||
expect(target).toBe('my_docs');
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeVisible();
|
||||
const responseMyDocs = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('documents/?page=1&is_creator_me=true') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
const resultMyDocs = await responseMyDocs.json();
|
||||
const countMyDocs = resultMyDocs.count as number;
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeHidden();
|
||||
expect(countMyDocs).toBeLessThanOrEqual(allCount);
|
||||
|
||||
// Shared with me
|
||||
await sharedWithMe.click();
|
||||
url = new URL(page.url());
|
||||
target = url.searchParams.get('target');
|
||||
expect(target).toBe('shared_with_me');
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeVisible();
|
||||
const responseSharedWithMe = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('documents/?page=1&is_creator_me=false') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
const resultSharedWithMe = await responseSharedWithMe.json();
|
||||
const countSharedWithMe = resultSharedWithMe.count as number;
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeHidden();
|
||||
expect(countSharedWithMe).toBeLessThanOrEqual(allCount);
|
||||
expect(countSharedWithMe + countMyDocs).toEqual(allCount);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Documents Grid', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('checks all the elements are visible', async ({ page }) => {
|
||||
let docs: SmallDoc[] = [];
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeVisible();
|
||||
|
||||
const response = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('documents/?page=1') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
const result = await response.json();
|
||||
docs = result.results as SmallDoc[];
|
||||
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeHidden();
|
||||
await expect(page.locator('h4').getByText('All docs')).toBeVisible();
|
||||
|
||||
const thead = page.getByTestId('docs-grid-header');
|
||||
await expect(thead.getByText(/Name/i)).toBeVisible();
|
||||
await expect(thead.getByText(/Updated at/i)).toBeVisible();
|
||||
|
||||
await Promise.all(
|
||||
docs.map(async (doc) => {
|
||||
await expect(
|
||||
page.getByTestId(`docs-grid-name-${doc.id}`),
|
||||
).toBeVisible();
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('checks the infinite scroll', async ({ page }) => {
|
||||
let docs: SmallDoc[] = [];
|
||||
const responsePromisePage1 = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/documents/?page=1`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
const responsePromisePage2 = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/documents/?page=2`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
const responsePage1 = await responsePromisePage1;
|
||||
expect(responsePage1.ok()).toBeTruthy();
|
||||
let result = await responsePage1.json();
|
||||
docs = result.results as SmallDoc[];
|
||||
await Promise.all(
|
||||
docs.map(async (doc) => {
|
||||
await expect(
|
||||
page.getByTestId(`docs-grid-name-${doc.id}`),
|
||||
).toBeVisible();
|
||||
}),
|
||||
);
|
||||
|
||||
await page.getByTestId('infinite-scroll-trigger').scrollIntoViewIfNeeded();
|
||||
|
||||
const responsePage2 = await responsePromisePage2;
|
||||
result = await responsePage2.json();
|
||||
docs = result.results as SmallDoc[];
|
||||
await Promise.all(
|
||||
docs.map(async (doc) => {
|
||||
await expect(
|
||||
page.getByTestId(`docs-grid-name-${doc.id}`),
|
||||
).toBeVisible();
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
mockedAccesses,
|
||||
mockedDocument,
|
||||
mockedInvitations,
|
||||
verifyDocName,
|
||||
} from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -59,81 +60,31 @@ test.describe('Doc Header', () => {
|
||||
const card = page.getByLabel(
|
||||
'It is the card information about the document.',
|
||||
);
|
||||
await expect(card.locator('a').getByText('home')).toBeVisible();
|
||||
await expect(card.locator('h2').getByText('Mocked document')).toBeVisible();
|
||||
await expect(card.getByText('Public')).toBeVisible();
|
||||
await expect(
|
||||
card.getByText('Created at 09/01/2021, 11:00 AM'),
|
||||
).toBeVisible();
|
||||
await expect(card.getByText('Your role: Owner')).toBeVisible();
|
||||
|
||||
const docTitle = card.getByRole('textbox', { name: 'doc title input' });
|
||||
await expect(docTitle).toBeVisible();
|
||||
|
||||
await expect(card.getByText('Public document')).toBeVisible();
|
||||
|
||||
await expect(card.getByText('Owner ·')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'download' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Open the document options' }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('it updates the title doc', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-update', browserName, 1);
|
||||
|
||||
await page.getByRole('heading', { name: randomDoc }).fill(' ');
|
||||
await page.getByText('Created at').click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Untitled document' }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('it updates the title doc from editor heading', async ({ page }) => {
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Create a new document',
|
||||
})
|
||||
.click();
|
||||
|
||||
const docHeader = page.getByLabel(
|
||||
'It is the card information about the document.',
|
||||
);
|
||||
|
||||
await expect(
|
||||
docHeader.getByRole('heading', { name: 'Untitled document', level: 2 }),
|
||||
).toBeVisible();
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
|
||||
await editor.locator('h1').click();
|
||||
await page.keyboard.type('Hello World', { delay: 100 });
|
||||
|
||||
await expect(
|
||||
docHeader.getByRole('heading', { name: 'Hello World', level: 2 }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByText('Document title updated successfully'),
|
||||
).toBeVisible();
|
||||
|
||||
await docHeader
|
||||
.getByRole('heading', { name: 'Hello World', level: 2 })
|
||||
.fill('Top World');
|
||||
|
||||
await editor.locator('h1').fill('Super World');
|
||||
|
||||
await expect(
|
||||
docHeader.getByRole('heading', { name: 'Top World', level: 2 }),
|
||||
).toBeVisible();
|
||||
|
||||
await editor.locator('h1').fill('');
|
||||
|
||||
await docHeader
|
||||
.getByRole('heading', { name: 'Top World', level: 2 })
|
||||
.fill(' ');
|
||||
|
||||
await page.getByText('Created at').click();
|
||||
|
||||
await expect(
|
||||
docHeader.getByRole('heading', { name: 'Untitled document', level: 2 }),
|
||||
).toBeVisible();
|
||||
await createDoc(page, 'doc-update', browserName, 1);
|
||||
const docTitle = page.getByRole('textbox', { name: 'doc title input' });
|
||||
await expect(docTitle).toBeVisible();
|
||||
await docTitle.fill('Hello World');
|
||||
await docTitle.blur();
|
||||
await verifyDocName(page, 'Hello World');
|
||||
});
|
||||
|
||||
test('it deletes the doc', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
@@ -156,9 +107,7 @@ test.describe('Doc Header', () => {
|
||||
page.getByText('The document has been deleted.'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Create a new document' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'New do' })).toBeVisible();
|
||||
|
||||
const row = page
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
@@ -192,16 +141,13 @@ test.describe('Doc Header', () => {
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(
|
||||
page.locator('h2').getByText('Mocked document'),
|
||||
).toHaveAttribute('contenteditable');
|
||||
await expect(page.getByRole('button', { name: 'download' })).toBeVisible();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Delete document' }),
|
||||
).toBeHidden();
|
||||
).toBeDisabled();
|
||||
|
||||
// Click somewhere else to close the options
|
||||
await page.click('body', { position: { x: 0, y: 0 } });
|
||||
@@ -270,16 +216,12 @@ test.describe('Doc Header', () => {
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(
|
||||
page.locator('h2').getByText('Mocked document'),
|
||||
).toHaveAttribute('contenteditable');
|
||||
|
||||
await expect(page.getByRole('button', { name: 'download' })).toBeVisible();
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Delete document' }),
|
||||
).toBeHidden();
|
||||
).toBeDisabled();
|
||||
|
||||
// Click somewhere else to close the options
|
||||
await page.click('body', { position: { x: 0, y: 0 } });
|
||||
@@ -348,16 +290,12 @@ test.describe('Doc Header', () => {
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(
|
||||
page.locator('h2').getByText('Mocked document'),
|
||||
).not.toHaveAttribute('contenteditable');
|
||||
|
||||
await expect(page.getByRole('button', { name: 'download' })).toBeVisible();
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Delete document' }),
|
||||
).toBeHidden();
|
||||
).toBeDisabled();
|
||||
|
||||
// Click somewhere else to close the options
|
||||
await page.click('body', { position: { x: 0, y: 0 } });
|
||||
@@ -411,7 +349,7 @@ test.describe('Doc Header', () => {
|
||||
// create page and navigate to it
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Create a new document',
|
||||
name: 'New doc',
|
||||
})
|
||||
.click();
|
||||
|
||||
@@ -446,7 +384,7 @@ test.describe('Doc Header', () => {
|
||||
// create page and navigate to it
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Create a new document',
|
||||
name: 'New doc',
|
||||
})
|
||||
.click();
|
||||
|
||||
@@ -498,6 +436,7 @@ test.describe('Documents Header mobile', () => {
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
await expect(page.getByLabel('Share modal')).toBeVisible();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
import { waitForElementCount } from '../helpers';
|
||||
|
||||
import { addNewMember, createDoc, goToGridDoc } from './common';
|
||||
import { addNewMember, createDoc, goToGridDoc, verifyDocName } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -54,8 +54,10 @@ test.describe('Document list members', () => {
|
||||
const list = page.getByLabel('List members card').locator('ul');
|
||||
await expect(list.locator('li')).toHaveCount(20);
|
||||
await list.getByText(`impress@impress.world-page-${1}-18`).hover();
|
||||
await page.mouse.wheel(0, 10);
|
||||
|
||||
const loadMoreButton = page
|
||||
.getByLabel('List members card')
|
||||
.getByRole('button', { name: 'arrow_downward Load more' });
|
||||
await loadMoreButton.scrollIntoViewIfNeeded();
|
||||
await waitForElementCount(list.locator('li'), 21, 10000);
|
||||
|
||||
expect(await list.locator('li').count()).toBeGreaterThan(20);
|
||||
@@ -109,9 +111,13 @@ test.describe('Document list members', () => {
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
const list = page.getByLabel('List invitation card').locator('ul');
|
||||
|
||||
await expect(list.locator('li')).toHaveCount(20);
|
||||
await list.getByText(`impress@impress.world-page-${1}-18`).hover();
|
||||
await page.mouse.wheel(0, 10);
|
||||
const loadMoreButton = page
|
||||
.getByLabel('List invitation card')
|
||||
.getByRole('button', { name: 'arrow_downward Load more' });
|
||||
await loadMoreButton.scrollIntoViewIfNeeded();
|
||||
|
||||
await waitForElementCount(list.locator('li'), 21, 10000);
|
||||
|
||||
@@ -127,7 +133,7 @@ test.describe('Document list members', () => {
|
||||
test('it checks the role rules', async ({ page, browserName }) => {
|
||||
const [docTitle] = await createDoc(page, 'Doc role rules', browserName, 1);
|
||||
|
||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
@@ -188,7 +194,7 @@ test.describe('Document list members', () => {
|
||||
test('it checks the delete members', async ({ page, browserName }) => {
|
||||
const [docTitle] = await createDoc(page, 'Doc role rules', browserName, 1);
|
||||
|
||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ test.describe('Doc Routing', () => {
|
||||
|
||||
test('Check the presence of the meta tag noindex', async ({ page }) => {
|
||||
const buttonCreateHomepage = page.getByRole('button', {
|
||||
name: 'Create a new document',
|
||||
name: 'New doc',
|
||||
});
|
||||
|
||||
await expect(buttonCreateHomepage).toBeVisible();
|
||||
@@ -27,7 +27,7 @@ test.describe('Doc Routing', () => {
|
||||
await expect(page).toHaveURL('/');
|
||||
|
||||
const buttonCreateHomepage = page.getByRole('button', {
|
||||
name: 'Create a new document',
|
||||
name: 'New doc',
|
||||
});
|
||||
|
||||
await expect(buttonCreateHomepage).toBeVisible();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, goToGridDoc } from './common';
|
||||
import { createDoc, verifyDocName } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -17,123 +17,60 @@ test.describe('Doc Table Content', () => {
|
||||
1,
|
||||
);
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Table of contents',
|
||||
})
|
||||
.click();
|
||||
|
||||
const panel = page.getByLabel('Document panel');
|
||||
const editor = page.locator('.ProseMirror');
|
||||
|
||||
await editor.locator('.bn-block-outer').last().fill('/');
|
||||
|
||||
await page.getByText('Heading 1').click();
|
||||
await page.keyboard.type('Hello World');
|
||||
await editor.getByText('Hello').dblclick();
|
||||
await page.keyboard.type('Level 1');
|
||||
await editor.getByText('Level 1').dblclick();
|
||||
await page.getByRole('button', { name: 'Strike' }).click();
|
||||
|
||||
await page.locator('.bn-block-outer').first().click();
|
||||
await editor.click();
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
|
||||
// Create space to fill the viewport
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
await editor.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Heading 2').click();
|
||||
await page.keyboard.type('Super World', { delay: 100 });
|
||||
await page.keyboard.type('Level 2');
|
||||
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
|
||||
// Create space to fill the viewport
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
await editor.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Heading 3').click();
|
||||
await page.keyboard.type('Another World');
|
||||
await page.keyboard.type('Level 3');
|
||||
|
||||
const hello = panel.getByText('Hello World');
|
||||
const superW = panel.getByText('Super World');
|
||||
const another = panel.getByText('Another World');
|
||||
expect(true).toBe(true);
|
||||
|
||||
await expect(hello).toBeVisible();
|
||||
await expect(hello).toHaveCSS('font-size', /17/);
|
||||
await expect(hello).toHaveAttribute('aria-selected', 'true');
|
||||
const summaryContainer = page.locator('#summaryContainer');
|
||||
await summaryContainer.hover();
|
||||
|
||||
await expect(superW).toBeVisible();
|
||||
await expect(superW).toHaveCSS('font-size', /14/);
|
||||
await expect(superW).toHaveAttribute('aria-selected', 'false');
|
||||
const level1 = summaryContainer.getByText('Level 1');
|
||||
const level2 = summaryContainer.getByText('Level 2');
|
||||
const level3 = summaryContainer.getByText('Level 3');
|
||||
|
||||
await expect(another).toBeVisible();
|
||||
await expect(another).toHaveCSS('font-size', /12/);
|
||||
await expect(another).toHaveAttribute('aria-selected', 'false');
|
||||
await expect(level1).toBeVisible();
|
||||
await expect(level1).toHaveCSS('padding', /4px 0px/);
|
||||
await expect(level1).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
await hello.click();
|
||||
await expect(level2).toBeVisible();
|
||||
await expect(level2).toHaveCSS('padding-left', /14.4px/);
|
||||
await expect(level2).toHaveAttribute('aria-selected', 'false');
|
||||
|
||||
await expect(editor.getByText('Hello World')).toBeInViewport();
|
||||
await expect(hello).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(superW).toHaveAttribute('aria-selected', 'false');
|
||||
|
||||
await another.click();
|
||||
|
||||
await expect(editor.getByText('Hello World')).not.toBeInViewport();
|
||||
await expect(hello).toHaveAttribute('aria-selected', 'false');
|
||||
await expect(superW).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
await panel.getByText('Back to top').click();
|
||||
await expect(editor.getByText('Hello World')).toBeInViewport();
|
||||
await expect(hello).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(superW).toHaveAttribute('aria-selected', 'false');
|
||||
|
||||
await panel.getByText('Go to bottom').click();
|
||||
await expect(editor.getByText('Hello World')).not.toBeInViewport();
|
||||
await expect(superW).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
test('it checks that table contents panel is opened automaticaly if more that 2 headings', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [randomDoc] = await createDoc(
|
||||
page,
|
||||
'doc-table-content',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
await expect(page.getByLabel('Open the panel')).toBeHidden();
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
|
||||
await editor.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Heading 1').click();
|
||||
await page.keyboard.type('Hello World', { delay: 100 });
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await editor.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Heading 2').click();
|
||||
await page.keyboard.type('Super World', { delay: 100 });
|
||||
|
||||
await goToGridDoc(page, {
|
||||
title: randomDoc,
|
||||
});
|
||||
|
||||
await expect(page.getByLabel('Close the panel')).toBeVisible();
|
||||
|
||||
const panel = page.getByLabel('Document panel');
|
||||
await expect(panel.getByText('Hello World')).toBeVisible();
|
||||
await expect(panel.getByText('Super World')).toBeVisible();
|
||||
|
||||
await page.getByLabel('Close the panel').click();
|
||||
|
||||
await expect(panel).toHaveAttribute('aria-hidden', 'true');
|
||||
await expect(level3).toBeVisible();
|
||||
await expect(level3).toHaveCSS('padding-left', /24px/);
|
||||
await expect(level3).toHaveAttribute('aria-selected', 'false');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, goToGridDoc, mockedDocument } from './common';
|
||||
import {
|
||||
createDoc,
|
||||
goToGridDoc,
|
||||
mockedDocument,
|
||||
verifyDocName,
|
||||
} from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -10,7 +15,7 @@ test.describe('Doc Version', () => {
|
||||
test('it displays the doc versions', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-version', browserName, 1);
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
@@ -18,24 +23,27 @@ test.describe('Doc Version', () => {
|
||||
name: 'Version history',
|
||||
})
|
||||
.click();
|
||||
await expect(page.getByText('History', { exact: true })).toBeVisible();
|
||||
|
||||
const panel = page.getByLabel('Document panel');
|
||||
|
||||
await expect(panel.getByText('Current version')).toBeVisible();
|
||||
expect(await panel.locator('li').count()).toBe(1);
|
||||
const modal = page.getByLabel('version history modal');
|
||||
const panel = modal.getByLabel('version list');
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(modal.getByText('No versions')).toBeVisible();
|
||||
|
||||
await modal.getByRole('button', { name: 'close' }).click();
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').last().fill('Hello World');
|
||||
|
||||
await goToGridDoc(page, {
|
||||
title: randomDoc,
|
||||
});
|
||||
|
||||
await expect(page.getByText('Hello World')).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Hello World' }),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.locator('.ProseMirror .bn-block')
|
||||
.getByText('Hello World')
|
||||
.getByRole('heading', { name: 'Hello World' })
|
||||
.fill('It will create a version');
|
||||
|
||||
await goToGridDoc(page, {
|
||||
@@ -43,7 +51,9 @@ test.describe('Doc Version', () => {
|
||||
});
|
||||
|
||||
await expect(page.getByText('Hello World')).toBeHidden();
|
||||
await expect(page.getByText('It will create a version')).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'It will create a version' }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
@@ -52,19 +62,16 @@ test.describe('Doc Version', () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(panel.getByText('Current version')).toBeVisible();
|
||||
expect(await panel.locator('li').count()).toBe(2);
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(page.getByText('History', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('status')).toBeVisible();
|
||||
await expect(page.getByRole('status')).toBeHidden();
|
||||
const items = await panel.locator('.version-item').all();
|
||||
expect(items.length).toBe(1);
|
||||
await items[0].click();
|
||||
|
||||
await panel.locator('li').nth(1).click();
|
||||
await expect(
|
||||
page.getByText('Read only, you cannot edit document versions.'),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText('Hello World')).toBeVisible();
|
||||
await expect(page.getByText('It will create a version')).toBeHidden();
|
||||
|
||||
await panel.getByText('Current version').click();
|
||||
await expect(page.getByText('Hello World')).toBeHidden();
|
||||
await expect(page.getByText('It will create a version')).toBeVisible();
|
||||
await expect(modal.getByText('Hello World')).toBeVisible();
|
||||
await expect(modal.getByText('It will create a version')).toBeHidden();
|
||||
});
|
||||
|
||||
test('it does not display the doc versions if not allowed', async ({
|
||||
@@ -79,24 +86,17 @@ test.describe('Doc Version', () => {
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
|
||||
await verifyDocName(page, 'Mocked document');
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Version history' }),
|
||||
).toBeHidden();
|
||||
|
||||
await page.getByRole('button', { name: 'Table of content' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByLabel('Document panel').getByText('Versions'),
|
||||
).toBeHidden();
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
test('it restores the doc version', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-version', browserName, 1);
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
await page.locator('.bn-block-outer').last().fill('Hello');
|
||||
@@ -124,84 +124,26 @@ test.describe('Doc Version', () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
const panel = page.getByLabel('Document panel');
|
||||
await panel.locator('li').nth(1).click();
|
||||
await expect(page.getByText('World')).toBeHidden();
|
||||
const modal = page.getByLabel('version history modal');
|
||||
const panel = modal.getByLabel('version list');
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
await panel.getByLabel('Open the version options').click();
|
||||
await page.getByText('Restore the version').click();
|
||||
await expect(page.getByText('History', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('status')).toBeVisible();
|
||||
await expect(page.getByRole('status')).toBeHidden();
|
||||
const items = await panel.locator('.version-item').all();
|
||||
expect(items.length).toBe(1);
|
||||
await items[0].click();
|
||||
|
||||
await expect(page.getByText('Restore this version?')).toBeVisible();
|
||||
await expect(modal.getByText('World')).toBeHidden();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Restore',
|
||||
})
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'Restore' }).click();
|
||||
await expect(page.getByText('Your current document will')).toBeVisible();
|
||||
await page.getByText('If a member is editing, his').click();
|
||||
|
||||
await expect(panel.locator('li')).toHaveCount(3);
|
||||
await page.getByLabel('Restore', { exact: true }).click();
|
||||
|
||||
await panel.getByText('Current version').click();
|
||||
await expect(page.getByText('Hello')).toBeVisible();
|
||||
await expect(page.getByText('World')).toBeHidden();
|
||||
});
|
||||
|
||||
test('it restores the doc version from button title', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-version', browserName, 1);
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
await editor.locator('.bn-block-outer').last().click();
|
||||
await editor.locator('.bn-block-outer').last().fill('Hello');
|
||||
|
||||
await goToGridDoc(page, {
|
||||
title: randomDoc,
|
||||
});
|
||||
|
||||
await expect(editor.getByText('Hello')).toBeVisible();
|
||||
await editor.locator('.bn-block-outer').last().click();
|
||||
await page.keyboard.press('Enter');
|
||||
await editor.locator('.bn-block-outer').last().fill('World');
|
||||
|
||||
await goToGridDoc(page, {
|
||||
title: randomDoc,
|
||||
});
|
||||
|
||||
await expect(editor.getByText('World')).toBeVisible();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Version history',
|
||||
})
|
||||
.click();
|
||||
|
||||
const panel = page.getByLabel('Document panel');
|
||||
await panel.locator('li').nth(1).click();
|
||||
await expect(editor.getByText('World')).toBeHidden();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Restore this version',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(page.getByText('Restore this version?')).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Restore',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(panel.locator('li')).toHaveCount(3);
|
||||
|
||||
await panel.getByText('Current version').click();
|
||||
await expect(editor.getByText('Hello')).toBeVisible();
|
||||
await expect(editor.getByText('World')).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, keyCloakSignIn } from './common';
|
||||
import { createDoc, keyCloakSignIn, verifyDocName } from './common';
|
||||
|
||||
const browsersName = ['chromium', 'webkit', 'firefox'];
|
||||
|
||||
@@ -85,7 +85,7 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
1,
|
||||
);
|
||||
|
||||
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
const urlDoc = page.url();
|
||||
|
||||
@@ -111,7 +111,7 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
|
||||
const [docTitle] = await createDoc(page, 'Restricted auth', browserName, 1);
|
||||
|
||||
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
const urlDoc = page.url();
|
||||
|
||||
@@ -139,7 +139,7 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
|
||||
const [docTitle] = await createDoc(page, 'Restricted auth', browserName, 1);
|
||||
|
||||
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
@@ -176,7 +176,7 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||
await verifyDocName(page, docTitle);
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -198,7 +198,7 @@ test.describe('Doc Visibility: Public', () => {
|
||||
1,
|
||||
);
|
||||
|
||||
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
await page
|
||||
@@ -227,10 +227,14 @@ test.describe('Doc Visibility: Public', () => {
|
||||
position: { x: 0, y: 0 },
|
||||
});
|
||||
|
||||
const cardContainer = page.getByLabel(
|
||||
'It is the card information about the document.',
|
||||
);
|
||||
|
||||
await expect(cardContainer.getByTestId('public-icon')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page
|
||||
.getByLabel('It is the card information about the document.')
|
||||
.getByText('Public', { exact: true }),
|
||||
cardContainer.getByText('Public document', { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
const urlDoc = page.url();
|
||||
@@ -261,7 +265,7 @@ test.describe('Doc Visibility: Public', () => {
|
||||
|
||||
const [docTitle] = await createDoc(page, 'Public editable', browserName, 1);
|
||||
|
||||
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
await page
|
||||
@@ -290,10 +294,14 @@ test.describe('Doc Visibility: Public', () => {
|
||||
position: { x: 0, y: 0 },
|
||||
});
|
||||
|
||||
const cardContainer = page.getByLabel(
|
||||
'It is the card information about the document.',
|
||||
);
|
||||
|
||||
await expect(cardContainer.getByTestId('public-icon')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page
|
||||
.getByLabel('It is the card information about the document.')
|
||||
.getByText('Public', { exact: true }),
|
||||
cardContainer.getByText('Public document', { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
const urlDoc = page.url();
|
||||
@@ -308,7 +316,7 @@ test.describe('Doc Visibility: Public', () => {
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||
await verifyDocName(page, docTitle);
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
|
||||
await expect(
|
||||
page.getByText('Read only, you cannot edit this document'),
|
||||
@@ -333,7 +341,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
1,
|
||||
);
|
||||
|
||||
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
await page
|
||||
@@ -385,7 +393,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
1,
|
||||
);
|
||||
|
||||
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
await page
|
||||
@@ -451,7 +459,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
1,
|
||||
);
|
||||
|
||||
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
await page
|
||||
@@ -498,7 +506,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||
await verifyDocName(page, docTitle);
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
await expect(
|
||||
page.getByText('Read only, you cannot edit this document'),
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { goToGridDoc } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Footer', () => {
|
||||
test('checks all the elements are visible', async ({ page }) => {
|
||||
const footer = page.locator('footer').first();
|
||||
|
||||
await expect(footer.getByAltText('Gouvernement Logo')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
footer.getByRole('link', { name: 'legifrance.gouv.fr' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
footer.getByRole('link', { name: 'info.gouv.fr' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
footer.getByRole('link', { name: 'service-public.fr' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
footer.getByRole('link', { name: 'data.gouv.fr' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
footer.getByRole('link', { name: 'Legal Notice' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
footer.getByRole('link', { name: 'Personal data and cookies' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
footer.getByRole('link', { name: 'Accessibility' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
footer.getByText(
|
||||
'Unless otherwise stated, all content on this site is under licence',
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks footer is not visible on doc editor', async ({ page }) => {
|
||||
await expect(page.locator('footer')).toBeVisible();
|
||||
await goToGridDoc(page);
|
||||
await expect(page.locator('footer')).toBeHidden();
|
||||
});
|
||||
|
||||
const legalPages = [
|
||||
{ name: 'Legal Notice', url: '/legal-notice/' },
|
||||
{ name: 'Personal data and cookies', url: '/personal-data-cookies/' },
|
||||
{ name: 'Accessibility', url: '/accessibility/' },
|
||||
];
|
||||
for (const { name, url } of legalPages) {
|
||||
test(`checks ${name} page`, async ({ page }) => {
|
||||
const footer = page.locator('footer').first();
|
||||
await footer.getByRole('link', { name }).click();
|
||||
|
||||
await expect(
|
||||
page
|
||||
.getByRole('heading', {
|
||||
name,
|
||||
})
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page).toHaveURL(url);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -75,29 +75,15 @@ test.describe('Header mobile', () => {
|
||||
test('it checks the header when mobile', async ({ page }) => {
|
||||
const header = page.locator('header').first();
|
||||
|
||||
await expect(header.getByLabel('Open the header menu')).toBeVisible();
|
||||
await expect(
|
||||
header.getByRole('link', { name: 'Docs Logo Docs' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
header.getByRole('button', {
|
||||
name: 'Les services de La Suite numérique',
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Logout',
|
||||
}),
|
||||
).toBeHidden();
|
||||
|
||||
await expect(page.getByText('English')).toBeHidden();
|
||||
|
||||
await header.getByLabel('Open the header menu').click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Logout',
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByText('English')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,11 +6,7 @@ test.beforeEach(async ({ page }) => {
|
||||
|
||||
test.describe('Language', () => {
|
||||
test('checks the language picker', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Create a new document',
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(page.getByLabel('Logout')).toBeVisible();
|
||||
|
||||
const header = page.locator('header').first();
|
||||
await header.getByRole('combobox').getByText('English').click();
|
||||
@@ -19,11 +15,7 @@ test.describe('Language', () => {
|
||||
header.getByRole('combobox').getByText('Français'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Créer un nouveau document',
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(page.getByLabel('Se déconnecter')).toBeVisible();
|
||||
|
||||
await header.getByRole('combobox').getByText('Français').click();
|
||||
await header.getByRole('option', { name: 'Deutsch' }).click();
|
||||
@@ -31,11 +23,7 @@ test.describe('Language', () => {
|
||||
header.getByRole('combobox').getByText('Deutsch'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Neues Dokument erstellen',
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(page.getByLabel('Abmelden')).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks that backend uses the same language as the frontend', async ({
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Left panel desktop', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Left panel mobile', () => {
|
||||
test.use({ viewport: { width: 500, height: 1200 } });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('checks all the desktop elements are hidden and all mobile elements are visible', async ({
|
||||
page,
|
||||
}) => {
|
||||
await expect(page.getByTestId('left-panel-desktop')).toBeHidden();
|
||||
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('combobox', { name: 'Language' });
|
||||
const logoutButton = page.getByRole('button', { name: 'Logout' });
|
||||
|
||||
await expect(homeButton).not.toBeInViewport();
|
||||
await expect(newDocButton).not.toBeInViewport();
|
||||
await expect(languageButton).not.toBeInViewport();
|
||||
await expect(logoutButton).not.toBeInViewport();
|
||||
|
||||
await header.getByLabel('Open the header menu').click();
|
||||
|
||||
await expect(page.getByTestId('left-panel-mobile')).toBeInViewport();
|
||||
await expect(homeButton).toBeInViewport();
|
||||
await expect(newDocButton).toBeInViewport();
|
||||
await expect(languageButton).toBeInViewport();
|
||||
await expect(logoutButton).toBeInViewport();
|
||||
});
|
||||
});
|
||||
@@ -9,10 +9,6 @@ server {
|
||||
try_files $uri index.html $uri/ =404;
|
||||
}
|
||||
|
||||
location ~ ^/docs/(.*)/versions/(.*)/$ {
|
||||
error_page 404 /docs/[id]/versions/[versionId]/;
|
||||
}
|
||||
|
||||
location /docs/ {
|
||||
error_page 404 /docs/[id]/;
|
||||
}
|
||||
|
||||
@@ -5,22 +5,30 @@ const config = {
|
||||
colors: {
|
||||
'card-border': '#ededed',
|
||||
'primary-bg': '#FAFAFA',
|
||||
'primary-050': '#F5F5FE',
|
||||
'primary-100': '#EDF5FA',
|
||||
'primary-150': '#E5EEFA',
|
||||
'primary-950': '#1B1B35',
|
||||
'info-150': '#E5EEFA',
|
||||
'greyscale-000': '#fff',
|
||||
'greyscale-1000': '#161616',
|
||||
},
|
||||
font: {
|
||||
sizes: {
|
||||
xs: '0.75rem',
|
||||
sm: '0.875rem',
|
||||
md: '1rem',
|
||||
lg: '1.125rem',
|
||||
ml: '0.938rem',
|
||||
xl: '1.50rem',
|
||||
xl: '1.25rem',
|
||||
t: '0.6875rem',
|
||||
s: '0.75rem',
|
||||
h1: '2.2rem',
|
||||
h2: '1.7rem',
|
||||
h3: '1.37rem',
|
||||
h4: '1.15rem',
|
||||
h5: '1rem',
|
||||
h6: '0.87rem',
|
||||
h1: '2rem',
|
||||
h2: '1.75rem',
|
||||
h3: '1.5rem',
|
||||
h4: '1.375rem',
|
||||
h5: '1.25rem',
|
||||
h6: '1.125rem',
|
||||
},
|
||||
weights: {
|
||||
thin: 100,
|
||||
@@ -34,6 +42,21 @@ const config = {
|
||||
auto: 'auto',
|
||||
bx: '2.2rem',
|
||||
full: '100%',
|
||||
'4xs': '0.125rem',
|
||||
'3xs': '0.25rem',
|
||||
'2xs': '0.375rem',
|
||||
xs: '0.5rem',
|
||||
sm: '0.75rem',
|
||||
base: '1rem',
|
||||
md: '1.5rem',
|
||||
lg: '2rem',
|
||||
xl: '2.5rem',
|
||||
xxl: '3rem',
|
||||
xxxl: '3.5rem',
|
||||
'4xl': '4rem',
|
||||
'5xl': '4.5rem',
|
||||
'6xl': '6rem',
|
||||
'7xl': '7.5rem',
|
||||
},
|
||||
breakpoints: {
|
||||
xxs: '320px',
|
||||
@@ -104,7 +127,7 @@ const config = {
|
||||
focus: 'var(--c--components--forms-select--border-radius)',
|
||||
},
|
||||
'font-size': 'var(--c--theme--font--sizes--ml)',
|
||||
'menu-background-color': '#ffffff',
|
||||
'menu-background-color': '#fff',
|
||||
'item-background-color': {
|
||||
hover: 'var(--c--theme--colors--primary-300)',
|
||||
},
|
||||
@@ -126,7 +149,7 @@ const config = {
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
'background-color': '#ffffff',
|
||||
'background-color': '#fff',
|
||||
},
|
||||
button: {
|
||||
'border-radius': {
|
||||
@@ -147,8 +170,8 @@ const config = {
|
||||
danger: {
|
||||
'color-hover': 'white',
|
||||
background: {
|
||||
color: 'var(--c--theme--colors--danger-400)',
|
||||
'color-hover': 'var(--c--theme--colors--danger-500)',
|
||||
color: 'var(--c--theme--colors--danger-600)',
|
||||
'color-hover': '#FF2725',
|
||||
'color-disabled': 'var(--c--theme--colors--danger-100)',
|
||||
},
|
||||
},
|
||||
@@ -178,7 +201,9 @@ const config = {
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
'color-disabled': 'var(--c--theme--colors--greyscale-600)',
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
color: 'var(--c--theme--colors--primary-100)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-300)',
|
||||
'color-active': 'var(--c--theme--colors--primary-100)',
|
||||
'color-disabled': 'var(--c--theme--colors--greyscale-200)',
|
||||
},
|
||||
},
|
||||
@@ -197,19 +222,19 @@ const config = {
|
||||
dsfr: {
|
||||
theme: {
|
||||
colors: {
|
||||
'card-border': '#ededed',
|
||||
'card-border': '#E5E5E5',
|
||||
'primary-text': '#000091',
|
||||
'primary-100': '#f5f5fe',
|
||||
'primary-100': '#ECECFE',
|
||||
'primary-150': '#F4F4FD',
|
||||
'primary-200': '#ececfe',
|
||||
'primary-300': '#e3e3fd',
|
||||
'primary-400': '#cacafb',
|
||||
'primary-500': '#6a6af4',
|
||||
'primary-600': '#000091',
|
||||
'primary-200': '#E3E3FD',
|
||||
'primary-300': '#CACAFB',
|
||||
'primary-400': '#8585F6',
|
||||
'primary-500': '#6A6AF4',
|
||||
'primary-600': '#313178',
|
||||
'primary-700': '#272747',
|
||||
'primary-800': '#21213f',
|
||||
'primary-900': '#1c1a36',
|
||||
'secondary-text': '#FFFFFF',
|
||||
'primary-800': '#000091',
|
||||
'primary-900': '#21213F',
|
||||
'secondary-text': '#fff',
|
||||
'secondary-100': '#fee9ea',
|
||||
'secondary-200': '#fedfdf',
|
||||
'secondary-300': '#fdbfbf',
|
||||
@@ -220,16 +245,22 @@ const config = {
|
||||
'secondary-800': '#341f1f',
|
||||
'secondary-900': '#2b1919',
|
||||
'greyscale-text': '#303C4B',
|
||||
'greyscale-000': '#f6f6f6',
|
||||
'greyscale-100': '#eeeeee',
|
||||
'greyscale-200': '#e5e5e5',
|
||||
'greyscale-300': '#e1e1e1',
|
||||
'greyscale-400': '#dddddd',
|
||||
'greyscale-500': '#cecece',
|
||||
'greyscale-600': '#7b7b7b',
|
||||
'greyscale-700': '#666666',
|
||||
'greyscale-800': '#2a2a2a',
|
||||
'greyscale-900': '#1e1e1e',
|
||||
'greyscale-000': '#fff',
|
||||
'greyscale-050': '#F6F6F6',
|
||||
'greyscale-100': '#eee',
|
||||
'greyscale-200': '#E5E5E5',
|
||||
'greyscale-250': '#ddd',
|
||||
'greyscale-300': '#CECECE',
|
||||
'greyscale-350': '#ddd',
|
||||
'greyscale-400': '#929292',
|
||||
'greyscale-500': '#7C7C7C',
|
||||
'greyscale-600': '#666666',
|
||||
'greyscale-700': '#3A3A3A',
|
||||
'greyscale-750': '#353535',
|
||||
'greyscale-800': '#2A2A2A',
|
||||
'greyscale-900': '#242424',
|
||||
'greyscale-950': '#1E1E1E',
|
||||
'greyscale-1000': '#161616',
|
||||
'success-text': '#1f8d49',
|
||||
'success-100': '#dffee6',
|
||||
'success-200': '#b8fec9',
|
||||
@@ -241,15 +272,15 @@ const config = {
|
||||
'success-800': '#1e2e22',
|
||||
'success-900': '#19281d',
|
||||
'info-text': '#0078f3',
|
||||
'info-100': '#f4f6ff',
|
||||
'info-200': '#e8edff',
|
||||
'info-300': '#dde5ff',
|
||||
'info-400': '#bdcdff',
|
||||
'info-500': '#0078f3',
|
||||
'info-600': '#0063cb',
|
||||
'info-700': '#f4f6ff',
|
||||
'info-800': '#222a3f',
|
||||
'info-900': '#1d2437',
|
||||
'info-100': '#E8EDFF',
|
||||
'info-200': '#DDE5FF',
|
||||
'info-300': '#BCCDFF',
|
||||
'info-400': '#518FFF',
|
||||
'info-500': '#0078F3',
|
||||
'info-600': '#0063CB',
|
||||
'info-700': '#273961',
|
||||
'info-800': '#222A3F',
|
||||
'info-900': '#1D2437',
|
||||
'warning-text': '#d64d00',
|
||||
'warning-100': '#fff4f3',
|
||||
'warning-200': '#ffe9e6',
|
||||
@@ -260,16 +291,16 @@ const config = {
|
||||
'warning-700': '#5e2c21',
|
||||
'warning-800': '#3e241e',
|
||||
'warning-900': '#361e19',
|
||||
'danger-text': '#e1000f',
|
||||
'danger-100': '#fef4f4',
|
||||
'danger-200': '#fee9e9',
|
||||
'danger-300': '#fddede',
|
||||
'danger-400': '#fcbfbf',
|
||||
'danger-500': '#e1000f',
|
||||
'danger-600': '#c9191e',
|
||||
'danger-700': '#642727',
|
||||
'danger-text': '#FFF',
|
||||
'danger-100': '#FFE9E9',
|
||||
'danger-200': '#FFDDDD',
|
||||
'danger-300': '#FFBDBD',
|
||||
'danger-400': '#FF5655',
|
||||
'danger-500': '#F60700',
|
||||
'danger-600': '#CE0500',
|
||||
'danger-700': '#642626',
|
||||
'danger-800': '#412121',
|
||||
'danger-900': '#3a1c1c',
|
||||
'danger-900': '#391C1C',
|
||||
},
|
||||
font: {
|
||||
families: {
|
||||
@@ -288,8 +319,12 @@ const config = {
|
||||
alert: {
|
||||
'border-radius': '0',
|
||||
},
|
||||
modal: {
|
||||
'width-small': '342px',
|
||||
},
|
||||
button: {
|
||||
'medium-height': '48px',
|
||||
'medium-height': '40px',
|
||||
'medium-text-height': '40px',
|
||||
'border-radius': '4px',
|
||||
primary: {
|
||||
background: {
|
||||
@@ -297,9 +332,9 @@ const config = {
|
||||
'color-hover': '#1212ff',
|
||||
'color-active': '#2323ff',
|
||||
},
|
||||
color: '#ffffff',
|
||||
'color-hover': '#ffffff',
|
||||
'color-active': '#ffffff',
|
||||
color: '#fff',
|
||||
'color-hover': '#fff',
|
||||
'color-active': '#fff',
|
||||
},
|
||||
'primary-text': {
|
||||
background: {
|
||||
@@ -321,7 +356,7 @@ const config = {
|
||||
},
|
||||
'tertiary-text': {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
'color-hover': 'var(--c--theme--colors--greyscale-100)',
|
||||
},
|
||||
'color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
color: 'var(--c--theme--colors--primary-600)',
|
||||
@@ -363,7 +398,7 @@ const config = {
|
||||
},
|
||||
'forms-input': {
|
||||
'border-radius': '4px',
|
||||
'background-color': '#ffffff',
|
||||
'background-color': '#fff',
|
||||
'border-color': 'var(--c--theme--colors--primary-text)',
|
||||
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
||||
'value-color': 'var(--c--theme--colors--primary-text)',
|
||||
@@ -381,7 +416,7 @@ const config = {
|
||||
'item-font-size': '14px',
|
||||
'border-radius': '4px',
|
||||
'border-radius-hover': '4px',
|
||||
'background-color': '#ffffff',
|
||||
'background-color': '#fff',
|
||||
'border-color': 'var(--c--theme--colors--primary-text)',
|
||||
'border-color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"react-aria-components": "1.5.0",
|
||||
"react-dom": "*",
|
||||
"react-i18next": "15.1.1",
|
||||
"react-intersection-observer": "9.13.1",
|
||||
"react-select": "5.8.3",
|
||||
"styled-components": "6.1.13",
|
||||
"y-protocols": "1.0.6",
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { AppWrapper } from '@/tests/utils';
|
||||
|
||||
import Page from '../pages';
|
||||
|
||||
jest.mock('next/router', () => ({
|
||||
useRouter() {
|
||||
return {
|
||||
push: jest.fn(),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@sentry/nextjs', () => ({
|
||||
captureException: jest.fn(),
|
||||
captureMessage: jest.fn(),
|
||||
setUser: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Page', () => {
|
||||
it('checks Page rendering', () => {
|
||||
render(<Page />, { wrapper: AppWrapper });
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: /Create a new document/i,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,13 @@
|
||||
import { ComponentPropsWithRef, forwardRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxType } from './Box';
|
||||
|
||||
export type BoxButtonType = ComponentPropsWithRef<typeof BoxButton>;
|
||||
export type BoxButtonType = BoxType & {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* Styleless button that extends the Box component.
|
||||
@@ -18,7 +22,7 @@ export type BoxButtonType = ComponentPropsWithRef<typeof BoxButton>;
|
||||
* </BoxButton>
|
||||
* ```
|
||||
*/
|
||||
const BoxButton = forwardRef<HTMLDivElement, BoxType>(
|
||||
const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
|
||||
({ $css, ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
@@ -28,14 +32,24 @@ const BoxButton = forwardRef<HTMLDivElement, BoxType>(
|
||||
$margin="none"
|
||||
$padding="none"
|
||||
$css={css`
|
||||
cursor: pointer;
|
||||
cursor: ${props.disabled ? 'not-allowed' : 'pointer'};
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: all 0.2s ease-in-out;
|
||||
font-family: inherit;
|
||||
|
||||
color: ${props.disabled
|
||||
? 'var(--c--theme--colors--greyscale-400) !important'
|
||||
: 'inherit'};
|
||||
${$css || ''}
|
||||
`}
|
||||
{...props}
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
props.onClick?.(event);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -17,8 +17,7 @@ export const Card = ({
|
||||
$background="white"
|
||||
$radius="4px"
|
||||
$css={css`
|
||||
box-shadow: 2px 2px 5px ${colorsTokens()['greyscale-300']};
|
||||
border: 1px solid ${colorsTokens()['card-border']};
|
||||
border: 1px solid ${colorsTokens()['greyscale-200']};
|
||||
${$css}
|
||||
`}
|
||||
{...props}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import React, {
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { PropsWithChildren, ReactNode, useEffect, useState } from 'react';
|
||||
import { Button, DialogTrigger, Popover } from 'react-aria-components';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@@ -11,7 +6,7 @@ const StyledPopover = styled(Popover)`
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
|
||||
padding: 0.5rem;
|
||||
|
||||
border: 1px solid #dddddd;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
@@ -29,7 +24,7 @@ const StyledButton = styled(Button)`
|
||||
text-wrap: nowrap;
|
||||
`;
|
||||
|
||||
interface DropButtonProps {
|
||||
export interface DropButtonProps {
|
||||
button: ReactNode;
|
||||
isOpen?: boolean;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
|
||||
117
src/frontend/apps/impress/src/components/DropdownMenu.tsx
Normal file
117
src/frontend/apps/impress/src/components/DropdownMenu.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { PropsWithChildren, useState } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, BoxProps, DropButton, Icon } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
export type DropdownMenuOption = {
|
||||
icon?: string;
|
||||
label: string;
|
||||
testId?: string;
|
||||
callback?: () => void | Promise<unknown>;
|
||||
danger?: boolean;
|
||||
disabled?: boolean;
|
||||
show?: boolean;
|
||||
};
|
||||
|
||||
export type DropdownMenuProps = {
|
||||
options: DropdownMenuOption[];
|
||||
showArrow?: boolean;
|
||||
arrowCss?: BoxProps['$css'];
|
||||
};
|
||||
|
||||
export const DropdownMenu = ({
|
||||
options,
|
||||
children,
|
||||
showArrow = false,
|
||||
arrowCss,
|
||||
}: PropsWithChildren<DropdownMenuProps>) => {
|
||||
const theme = useCunninghamTheme();
|
||||
const spacings = theme.spacingsTokens();
|
||||
const colors = theme.colorsTokens();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const onOpenChange = (isOpen: boolean) => {
|
||||
setIsOpen(isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<DropButton
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
button={
|
||||
showArrow ? (
|
||||
<Box>
|
||||
<div>{children}</div>
|
||||
<Icon
|
||||
$css={
|
||||
arrowCss ??
|
||||
css`
|
||||
color: var(--c--theme--colors--primary-600);
|
||||
`
|
||||
}
|
||||
iconName={isOpen ? 'arrow_drop_up' : 'arrow_drop_down'}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
children
|
||||
)
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
{options.map((option, index) => {
|
||||
if (option.show !== undefined && !option.show) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isDisabled = option.disabled !== undefined && option.disabled;
|
||||
return (
|
||||
<BoxButton
|
||||
data-testid={option.testId}
|
||||
$direction="row"
|
||||
disabled={isDisabled}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onOpenChange?.(false);
|
||||
void option.callback?.();
|
||||
}}
|
||||
key={option.label}
|
||||
$align="center"
|
||||
$background={colors['greyscale-000']}
|
||||
$color={colors['primary-600']}
|
||||
$padding={{ vertical: 'xs', horizontal: 'base' }}
|
||||
$width="100%"
|
||||
$gap={spacings['base']}
|
||||
$css={css`
|
||||
border: none;
|
||||
font-size: var(--c--theme--font--sizes--sm);
|
||||
color: var(--c--theme--colors--primary-600);
|
||||
font-weight: 500;
|
||||
cursor: ${isDisabled ? 'not-allowed' : 'pointer'};
|
||||
user-select: none;
|
||||
border-bottom: ${index !== options.length - 1
|
||||
? `1px solid var(--c--theme--colors--greyscale-200)`
|
||||
: 'none'};
|
||||
|
||||
&:hover {
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
}
|
||||
`}
|
||||
>
|
||||
{option.icon && (
|
||||
<Icon
|
||||
$size="20px"
|
||||
$theme={!isDisabled ? 'primary' : 'greyscale'}
|
||||
$variation={!isDisabled ? '600' : '400'}
|
||||
iconName={option.icon}
|
||||
/>
|
||||
)}
|
||||
{option.label}
|
||||
</BoxButton>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</DropButton>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,19 @@
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Text, TextType } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
type IconProps = TextType & {
|
||||
iconName: string;
|
||||
};
|
||||
export const Icon = ({ iconName, ...textProps }: IconProps) => {
|
||||
return (
|
||||
<Text $isMaterialIcon {...textProps}>
|
||||
{iconName}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
interface IconBGProps extends TextType {
|
||||
iconName: string;
|
||||
}
|
||||
@@ -29,23 +42,21 @@ export const IconBG = ({ iconName, ...textProps }: IconBGProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
interface IconOptionsProps {
|
||||
isOpen: boolean;
|
||||
'aria-label': string;
|
||||
}
|
||||
type IconOptionsProps = TextType & {
|
||||
isHorizontal?: boolean;
|
||||
};
|
||||
|
||||
export const IconOptions = ({ isOpen, ...props }: IconOptionsProps) => {
|
||||
export const IconOptions = ({ isHorizontal, ...props }: IconOptionsProps) => {
|
||||
return (
|
||||
<Text
|
||||
aria-label={props['aria-label']}
|
||||
{...props}
|
||||
$isMaterialIcon
|
||||
$css={`
|
||||
transition: all 0.3s ease-in-out;
|
||||
transform: rotate(${isOpen ? '90' : '0'}deg);
|
||||
$css={css`
|
||||
user-select: none;
|
||||
${props.$css}
|
||||
`}
|
||||
>
|
||||
more_vert
|
||||
{isHorizontal ? 'more_horiz' : 'more_vert'}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { PropsWithChildren, useEffect, useRef } from 'react';
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { InView } from 'react-intersection-observer';
|
||||
|
||||
import { Box, BoxType } from '@/components';
|
||||
import { Box, BoxType, Icon } from '@/components';
|
||||
|
||||
interface InfiniteScrollProps extends BoxType {
|
||||
hasMore: boolean;
|
||||
isLoading: boolean;
|
||||
next: () => void;
|
||||
scrollContainer: HTMLElement | null;
|
||||
scrollContainer?: HTMLElement | null;
|
||||
buttonLabel?: string;
|
||||
}
|
||||
|
||||
export const InfiniteScroll = ({
|
||||
@@ -14,42 +18,31 @@ export const InfiniteScroll = ({
|
||||
hasMore,
|
||||
isLoading,
|
||||
next,
|
||||
scrollContainer,
|
||||
buttonLabel,
|
||||
...boxProps
|
||||
}: PropsWithChildren<InfiniteScrollProps>) => {
|
||||
const timeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrollContainer) {
|
||||
const { t } = useTranslation();
|
||||
const loadMore = (inView: boolean) => {
|
||||
if (!inView || isLoading) {
|
||||
return;
|
||||
}
|
||||
void next();
|
||||
};
|
||||
|
||||
const nextHandle = () => {
|
||||
if (!hasMore || isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// To not wait until the end of the scroll to load more data
|
||||
const heightFromBottom = 150;
|
||||
|
||||
const { scrollTop, clientHeight, scrollHeight } = scrollContainer;
|
||||
if (scrollTop + clientHeight >= scrollHeight - heightFromBottom) {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (timeout.current) {
|
||||
clearTimeout(timeout.current);
|
||||
}
|
||||
timeout.current = setTimeout(nextHandle, 50);
|
||||
};
|
||||
|
||||
scrollContainer.addEventListener('scroll', handleScroll);
|
||||
return () => {
|
||||
scrollContainer.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [hasMore, isLoading, next, scrollContainer]);
|
||||
|
||||
return <Box {...boxProps}>{children}</Box>;
|
||||
return (
|
||||
<Box {...boxProps}>
|
||||
{children}
|
||||
<InView onChange={loadMore}>
|
||||
{!isLoading && hasMore && (
|
||||
<Button
|
||||
onClick={() => void next()}
|
||||
color="primary-text"
|
||||
icon={<Icon iconName="arrow_downward" />}
|
||||
>
|
||||
{buttonLabel ?? t('Load more')}
|
||||
</Button>
|
||||
)}
|
||||
</InView>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface TextProps extends BoxProps {
|
||||
| 'greyscale';
|
||||
$variation?:
|
||||
| 'text'
|
||||
| '000'
|
||||
| '100'
|
||||
| '200'
|
||||
| '300'
|
||||
@@ -41,7 +42,8 @@ export interface TextProps extends BoxProps {
|
||||
| '600'
|
||||
| '700'
|
||||
| '800'
|
||||
| '900';
|
||||
| '900'
|
||||
| '1000';
|
||||
}
|
||||
|
||||
export type TextType = ComponentPropsWithRef<typeof Text>;
|
||||
|
||||
@@ -17,8 +17,8 @@ describe('<Box />', () => {
|
||||
);
|
||||
|
||||
expect(screen.getByText('My Box')).toHaveStyle(`
|
||||
padding-left: 4rem;
|
||||
padding-right: 4rem;
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
padding-top: 3rem;
|
||||
padding-bottom: 0.5rem;`);
|
||||
});
|
||||
|
||||
@@ -2,9 +2,11 @@ export * from './Box';
|
||||
export * from './BoxButton';
|
||||
export * from './Card';
|
||||
export * from './DropButton';
|
||||
export * from './DropdownMenu';
|
||||
export * from './Icon';
|
||||
export * from './InfiniteScroll';
|
||||
export * from './Link';
|
||||
export * from './SideModal';
|
||||
export * from './separators/SeparatedSection';
|
||||
export * from './Text';
|
||||
export * from './TextErrors';
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { Box } from '../Box';
|
||||
|
||||
export enum SeparatorVariant {
|
||||
LIGHT = 'light',
|
||||
DARK = 'dark',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
variant?: SeparatorVariant;
|
||||
$withPadding?: boolean;
|
||||
};
|
||||
|
||||
export const HorizontalSeparator = ({
|
||||
variant = SeparatorVariant.LIGHT,
|
||||
$withPadding = true,
|
||||
}: Props) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
$height="1px"
|
||||
$width="100%"
|
||||
$margin={{ vertical: $withPadding ? 'base' : 'none' }}
|
||||
$background={
|
||||
variant === SeparatorVariant.DARK
|
||||
? '#e5e5e533'
|
||||
: colorsTokens()['greyscale-100']
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { Box } from '../Box';
|
||||
|
||||
type Props = {
|
||||
showSeparator?: boolean;
|
||||
};
|
||||
|
||||
export const SeparatedSection = ({
|
||||
showSeparator = true,
|
||||
children,
|
||||
}: PropsWithChildren<Props>) => {
|
||||
const theme = useCunninghamTheme();
|
||||
const colors = theme.colorsTokens();
|
||||
const spacings = theme.spacingsTokens();
|
||||
return (
|
||||
<Box
|
||||
$css={css`
|
||||
padding: ${spacings['base']} 0;
|
||||
${showSeparator &&
|
||||
css`
|
||||
border-bottom: 1px solid ${colors?.['greyscale-200']};
|
||||
`}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -351,6 +351,24 @@ input:-webkit-autofill:focus {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.c__button--nano {
|
||||
padding: 0 var(--c--theme--spacings--3xs);
|
||||
gap: var(--c--theme--spacings--3xs);
|
||||
}
|
||||
|
||||
.c__button--nano {
|
||||
padding: 0 var(--c--theme--spacings--3xs);
|
||||
gap: var(--c--theme--spacings--3xs);
|
||||
}
|
||||
|
||||
.c__button--nano.c__button--icon-only {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.c__button--nano.c__button--icon-only.c__button--full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c__button--medium {
|
||||
padding: 0.9rem var(--c--theme--spacings--s);
|
||||
}
|
||||
@@ -442,6 +460,7 @@ input:-webkit-autofill:focus {
|
||||
}
|
||||
|
||||
.c__button--tertiary {
|
||||
background-color: var(--c--components--button--tertiary--background--color);
|
||||
color: var(--c--components--button--tertiary--color);
|
||||
border: none;
|
||||
}
|
||||
@@ -454,6 +473,13 @@ input:-webkit-autofill:focus {
|
||||
color: var(--c--components--button--tertiary--color);
|
||||
}
|
||||
|
||||
.c__button--tertiary:active {
|
||||
background-color: var(
|
||||
--c--components--button--tertiary--background--color-active
|
||||
);
|
||||
color: var(--c--components--button--tertiary--color-active);
|
||||
}
|
||||
|
||||
.c__button--tertiary:disabled {
|
||||
background-color: var(
|
||||
--c--components--button--tertiary--background--color-disabled
|
||||
@@ -511,14 +537,32 @@ input:-webkit-autofill:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.c__modal__footer {
|
||||
margin-top: -1.5rem;
|
||||
}
|
||||
|
||||
.c__modal__close button {
|
||||
padding: 1.5rem 1rem;
|
||||
padding: 0px;
|
||||
font-size: 88px;
|
||||
width: 28px !important;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.c__modal__close button .material-icons {
|
||||
padding: 0px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.c__modal--full .c__modal__content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.c__modal__title {
|
||||
padding: 0;
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: var(--c--theme--spacings--sm);
|
||||
}
|
||||
|
||||
@media screen and (width <= 420px) {
|
||||
.c__modal__scroller {
|
||||
padding: 0.7rem;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ export const tokens = {
|
||||
'secondary-700': '#97A3AE',
|
||||
'secondary-800': '#757E87',
|
||||
'secondary-900': '#596067',
|
||||
'info-text': '#FFFFFF',
|
||||
'info-text': '#fff',
|
||||
'info-100': '#EBF2FC',
|
||||
'info-200': '#8CB5EA',
|
||||
'info-300': '#5894E1',
|
||||
@@ -32,7 +32,7 @@ export const tokens = {
|
||||
'greyscale-700': '#555F6B',
|
||||
'greyscale-800': '#303C4B',
|
||||
'greyscale-900': '#0C1A2B',
|
||||
'greyscale-000': '#FFFFFF',
|
||||
'greyscale-000': '#fff',
|
||||
'primary-100': '#EDF5FA',
|
||||
'primary-200': '#8CB5EA',
|
||||
'primary-300': '#5894E1',
|
||||
@@ -69,28 +69,35 @@ export const tokens = {
|
||||
'danger-700': '#9B0000',
|
||||
'danger-800': '#780000',
|
||||
'danger-900': '#5C0000',
|
||||
'primary-text': '#FFFFFF',
|
||||
'success-text': '#FFFFFF',
|
||||
'warning-text': '#FFFFFF',
|
||||
'danger-text': '#FFFFFF',
|
||||
'primary-text': '#fff',
|
||||
'success-text': '#fff',
|
||||
'warning-text': '#fff',
|
||||
'danger-text': '#fff',
|
||||
'card-border': '#ededed',
|
||||
'primary-bg': '#FAFAFA',
|
||||
'primary-050': '#F5F5FE',
|
||||
'primary-150': '#E5EEFA',
|
||||
'primary-950': '#1B1B35',
|
||||
'info-150': '#E5EEFA',
|
||||
'greyscale-1000': '#161616',
|
||||
},
|
||||
font: {
|
||||
sizes: {
|
||||
h1: '2.2rem',
|
||||
h2: '1.7rem',
|
||||
h3: '1.37rem',
|
||||
h4: '1.15rem',
|
||||
h5: '1rem',
|
||||
h6: '0.87rem',
|
||||
h1: '2rem',
|
||||
h2: '1.75rem',
|
||||
h3: '1.5rem',
|
||||
h4: '1.375rem',
|
||||
h5: '1.25rem',
|
||||
h6: '1.125rem',
|
||||
l: '1rem',
|
||||
m: '0.8125rem',
|
||||
s: '0.75rem',
|
||||
xs: '0.75rem',
|
||||
sm: '0.875rem',
|
||||
md: '1rem',
|
||||
lg: '1.125rem',
|
||||
ml: '0.938rem',
|
||||
xl: '1.50rem',
|
||||
xl: '1.25rem',
|
||||
t: '0.6875rem',
|
||||
},
|
||||
weights: {
|
||||
@@ -120,7 +127,7 @@ export const tokens = {
|
||||
},
|
||||
spacings: {
|
||||
'0': '0',
|
||||
xl: '4rem',
|
||||
xl: '2.5rem',
|
||||
l: '3rem',
|
||||
b: '1.625rem',
|
||||
s: '1rem',
|
||||
@@ -130,6 +137,20 @@ export const tokens = {
|
||||
auto: 'auto',
|
||||
bx: '2.2rem',
|
||||
full: '100%',
|
||||
'4xs': '0.125rem',
|
||||
'3xs': '0.25rem',
|
||||
'2xs': '0.375rem',
|
||||
xs: '0.5rem',
|
||||
sm: '0.75rem',
|
||||
base: '1rem',
|
||||
md: '1.5rem',
|
||||
lg: '2rem',
|
||||
xxl: '3rem',
|
||||
xxxl: '3.5rem',
|
||||
'4xl': '4rem',
|
||||
'5xl': '4.5rem',
|
||||
'6xl': '6rem',
|
||||
'7xl': '7.5rem',
|
||||
},
|
||||
transitions: {
|
||||
'ease-in': 'cubic-bezier(0.32, 0, 0.67, 0)',
|
||||
@@ -202,7 +223,7 @@ export const tokens = {
|
||||
focus: 'var(--c--components--forms-select--border-radius)',
|
||||
},
|
||||
'font-size': 'var(--c--theme--font--sizes--ml)',
|
||||
'menu-background-color': '#ffffff',
|
||||
'menu-background-color': '#fff',
|
||||
'item-background-color': {
|
||||
hover: 'var(--c--theme--colors--primary-300)',
|
||||
},
|
||||
@@ -223,7 +244,7 @@ export const tokens = {
|
||||
'border-color-hover': 'var(--c--theme--colors--greyscale-200)',
|
||||
},
|
||||
},
|
||||
modal: { 'background-color': '#ffffff' },
|
||||
modal: { 'background-color': '#fff' },
|
||||
button: {
|
||||
'border-radius': {
|
||||
active: 'var(--c--components--button--border-radius)',
|
||||
@@ -243,8 +264,8 @@ export const tokens = {
|
||||
danger: {
|
||||
'color-hover': 'white',
|
||||
background: {
|
||||
color: 'var(--c--theme--colors--danger-400)',
|
||||
'color-hover': 'var(--c--theme--colors--danger-500)',
|
||||
color: 'var(--c--theme--colors--danger-600)',
|
||||
'color-hover': '#FF2725',
|
||||
'color-disabled': 'var(--c--theme--colors--danger-100)',
|
||||
},
|
||||
},
|
||||
@@ -270,7 +291,9 @@ export const tokens = {
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
'color-disabled': 'var(--c--theme--colors--greyscale-600)',
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
color: 'var(--c--theme--colors--primary-100)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-300)',
|
||||
'color-active': 'var(--c--theme--colors--primary-100)',
|
||||
'color-disabled': 'var(--c--theme--colors--greyscale-200)',
|
||||
},
|
||||
},
|
||||
@@ -334,19 +357,19 @@ export const tokens = {
|
||||
dsfr: {
|
||||
theme: {
|
||||
colors: {
|
||||
'card-border': '#ededed',
|
||||
'card-border': '#E5E5E5',
|
||||
'primary-text': '#000091',
|
||||
'primary-100': '#f5f5fe',
|
||||
'primary-100': '#ECECFE',
|
||||
'primary-150': '#F4F4FD',
|
||||
'primary-200': '#ececfe',
|
||||
'primary-300': '#e3e3fd',
|
||||
'primary-400': '#cacafb',
|
||||
'primary-500': '#6a6af4',
|
||||
'primary-600': '#000091',
|
||||
'primary-200': '#E3E3FD',
|
||||
'primary-300': '#CACAFB',
|
||||
'primary-400': '#8585F6',
|
||||
'primary-500': '#6A6AF4',
|
||||
'primary-600': '#313178',
|
||||
'primary-700': '#272747',
|
||||
'primary-800': '#21213f',
|
||||
'primary-900': '#1c1a36',
|
||||
'secondary-text': '#FFFFFF',
|
||||
'primary-800': '#000091',
|
||||
'primary-900': '#21213F',
|
||||
'secondary-text': '#fff',
|
||||
'secondary-100': '#fee9ea',
|
||||
'secondary-200': '#fedfdf',
|
||||
'secondary-300': '#fdbfbf',
|
||||
@@ -357,16 +380,22 @@ export const tokens = {
|
||||
'secondary-800': '#341f1f',
|
||||
'secondary-900': '#2b1919',
|
||||
'greyscale-text': '#303C4B',
|
||||
'greyscale-000': '#f6f6f6',
|
||||
'greyscale-100': '#eeeeee',
|
||||
'greyscale-200': '#e5e5e5',
|
||||
'greyscale-300': '#e1e1e1',
|
||||
'greyscale-400': '#dddddd',
|
||||
'greyscale-500': '#cecece',
|
||||
'greyscale-600': '#7b7b7b',
|
||||
'greyscale-700': '#666666',
|
||||
'greyscale-800': '#2a2a2a',
|
||||
'greyscale-900': '#1e1e1e',
|
||||
'greyscale-000': '#fff',
|
||||
'greyscale-050': '#F6F6F6',
|
||||
'greyscale-100': '#eee',
|
||||
'greyscale-200': '#E5E5E5',
|
||||
'greyscale-250': '#ddd',
|
||||
'greyscale-300': '#CECECE',
|
||||
'greyscale-350': '#ddd',
|
||||
'greyscale-400': '#929292',
|
||||
'greyscale-500': '#7C7C7C',
|
||||
'greyscale-600': '#666666',
|
||||
'greyscale-700': '#3A3A3A',
|
||||
'greyscale-750': '#353535',
|
||||
'greyscale-800': '#2A2A2A',
|
||||
'greyscale-900': '#242424',
|
||||
'greyscale-950': '#1E1E1E',
|
||||
'greyscale-1000': '#161616',
|
||||
'success-text': '#1f8d49',
|
||||
'success-100': '#dffee6',
|
||||
'success-200': '#b8fec9',
|
||||
@@ -378,15 +407,15 @@ export const tokens = {
|
||||
'success-800': '#1e2e22',
|
||||
'success-900': '#19281d',
|
||||
'info-text': '#0078f3',
|
||||
'info-100': '#f4f6ff',
|
||||
'info-200': '#e8edff',
|
||||
'info-300': '#dde5ff',
|
||||
'info-400': '#bdcdff',
|
||||
'info-500': '#0078f3',
|
||||
'info-600': '#0063cb',
|
||||
'info-700': '#f4f6ff',
|
||||
'info-800': '#222a3f',
|
||||
'info-900': '#1d2437',
|
||||
'info-100': '#E8EDFF',
|
||||
'info-200': '#DDE5FF',
|
||||
'info-300': '#BCCDFF',
|
||||
'info-400': '#518FFF',
|
||||
'info-500': '#0078F3',
|
||||
'info-600': '#0063CB',
|
||||
'info-700': '#273961',
|
||||
'info-800': '#222A3F',
|
||||
'info-900': '#1D2437',
|
||||
'warning-text': '#d64d00',
|
||||
'warning-100': '#fff4f3',
|
||||
'warning-200': '#ffe9e6',
|
||||
@@ -397,16 +426,16 @@ export const tokens = {
|
||||
'warning-700': '#5e2c21',
|
||||
'warning-800': '#3e241e',
|
||||
'warning-900': '#361e19',
|
||||
'danger-text': '#e1000f',
|
||||
'danger-100': '#fef4f4',
|
||||
'danger-200': '#fee9e9',
|
||||
'danger-300': '#fddede',
|
||||
'danger-400': '#fcbfbf',
|
||||
'danger-500': '#e1000f',
|
||||
'danger-600': '#c9191e',
|
||||
'danger-700': '#642727',
|
||||
'danger-text': '#FFF',
|
||||
'danger-100': '#FFE9E9',
|
||||
'danger-200': '#FFDDDD',
|
||||
'danger-300': '#FFBDBD',
|
||||
'danger-400': '#FF5655',
|
||||
'danger-500': '#F60700',
|
||||
'danger-600': '#CE0500',
|
||||
'danger-700': '#642626',
|
||||
'danger-800': '#412121',
|
||||
'danger-900': '#3a1c1c',
|
||||
'danger-900': '#391C1C',
|
||||
},
|
||||
font: { families: { accent: 'Marianne', base: 'Marianne' } },
|
||||
logo: {
|
||||
@@ -418,8 +447,10 @@ export const tokens = {
|
||||
},
|
||||
components: {
|
||||
alert: { 'border-radius': '0' },
|
||||
modal: { 'width-small': '342px' },
|
||||
button: {
|
||||
'medium-height': '48px',
|
||||
'medium-height': '40px',
|
||||
'medium-text-height': '40px',
|
||||
'border-radius': '4px',
|
||||
primary: {
|
||||
background: {
|
||||
@@ -427,9 +458,9 @@ export const tokens = {
|
||||
'color-hover': '#1212ff',
|
||||
'color-active': '#2323ff',
|
||||
},
|
||||
color: '#ffffff',
|
||||
'color-hover': '#ffffff',
|
||||
'color-active': '#ffffff',
|
||||
color: '#fff',
|
||||
'color-hover': '#fff',
|
||||
'color-active': '#fff',
|
||||
},
|
||||
'primary-text': {
|
||||
background: {
|
||||
@@ -448,7 +479,7 @@ export const tokens = {
|
||||
},
|
||||
'tertiary-text': {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
'color-hover': 'var(--c--theme--colors--greyscale-100)',
|
||||
},
|
||||
'color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
color: 'var(--c--theme--colors--primary-600)',
|
||||
@@ -486,7 +517,7 @@ export const tokens = {
|
||||
},
|
||||
'forms-input': {
|
||||
'border-radius': '4px',
|
||||
'background-color': '#ffffff',
|
||||
'background-color': '#fff',
|
||||
'border-color': 'var(--c--theme--colors--primary-text)',
|
||||
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
||||
'value-color': 'var(--c--theme--colors--primary-text)',
|
||||
@@ -502,7 +533,7 @@ export const tokens = {
|
||||
'item-font-size': '14px',
|
||||
'border-radius': '4px',
|
||||
'border-radius-hover': '4px',
|
||||
'background-color': '#ffffff',
|
||||
'background-color': '#fff',
|
||||
'border-color': 'var(--c--theme--colors--primary-text)',
|
||||
'border-color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
||||
|
||||
@@ -5,6 +5,8 @@ import { tokens } from './cunningham-tokens';
|
||||
|
||||
type Tokens = typeof tokens.themes.default & Partial<typeof tokens.themes.dsfr>;
|
||||
type ColorsTokens = Tokens['theme']['colors'];
|
||||
type FontSizesTokens = Tokens['theme']['font']['sizes'];
|
||||
type SpacingsTokens = Tokens['theme']['spacings'];
|
||||
type ComponentTokens = Tokens['components'];
|
||||
export type Theme = keyof typeof tokens.themes;
|
||||
|
||||
@@ -13,6 +15,8 @@ interface AuthStore {
|
||||
setTheme: (theme: Theme) => void;
|
||||
themeTokens: () => Partial<Tokens['theme']>;
|
||||
colorsTokens: () => Partial<ColorsTokens>;
|
||||
fontSizesTokens: () => Partial<FontSizesTokens>;
|
||||
spacingsTokens: () => Partial<SpacingsTokens>;
|
||||
componentTokens: () => ComponentTokens;
|
||||
}
|
||||
|
||||
@@ -28,6 +32,8 @@ export const useCunninghamTheme = create<AuthStore>((set, get) => {
|
||||
themeTokens: () => currentTheme().theme,
|
||||
colorsTokens: () => currentTheme().theme.colors,
|
||||
componentTokens: () => currentTheme().components,
|
||||
spacingsTokens: () => currentTheme().theme.spacings,
|
||||
fontSizesTokens: () => currentTheme().theme.font.sizes,
|
||||
setTheme: (theme: Theme) => {
|
||||
set({ theme });
|
||||
},
|
||||
|
||||
@@ -4,8 +4,9 @@ import { BlockNoteView } from '@blocknote/mantine';
|
||||
import '@blocknote/mantine/style.css';
|
||||
import { useCreateBlockNote } from '@blocknote/react';
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, TextErrors } from '@/components';
|
||||
import { useAuthStore } from '@/core/auth';
|
||||
@@ -18,14 +19,35 @@ import { randomColor } from '../utils';
|
||||
|
||||
import { BlockNoteToolbar } from './BlockNoteToolbar';
|
||||
|
||||
const cssEditor = (readonly: boolean) => `
|
||||
&, & > .bn-container, & .ProseMirror {
|
||||
height:100%
|
||||
};
|
||||
& .bn-editor {
|
||||
padding-right: 30px;
|
||||
${readonly && `padding-left: 30px;`}
|
||||
};
|
||||
const cssEditor = (readonly: boolean) => css`
|
||||
color: red;
|
||||
&,
|
||||
& > .bn-container,
|
||||
& .ProseMirror {
|
||||
height: 100%;
|
||||
a {
|
||||
color: var(--c--theme--colors--greyscale-500);
|
||||
cursor: pointer;
|
||||
}
|
||||
.bn-block-group
|
||||
.bn-block-group
|
||||
.bn-block-outer:not([data-prev-depth-changed]):before {
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bn-block-outer:not(:first-child) {
|
||||
&:has(h1) {
|
||||
padding-top: 32px;
|
||||
}
|
||||
&:has(h2) {
|
||||
padding-top: 24px;
|
||||
}
|
||||
&:has(h3) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
& .bn-inline-content code {
|
||||
background-color: gainsboro;
|
||||
padding: 2px;
|
||||
@@ -36,14 +58,14 @@ const cssEditor = (readonly: boolean) => `
|
||||
padding-left: 40px;
|
||||
padding-right: 10px;
|
||||
${readonly && `padding-left: 10px;`}
|
||||
};
|
||||
.bn-side-menu[data-block-type=heading][data-level="1"] {
|
||||
height: 46px;
|
||||
}
|
||||
.bn-side-menu[data-block-type=heading][data-level="2"] {
|
||||
.bn-side-menu[data-block-type='heading'][data-level='1'] {
|
||||
height: 46px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='heading'][data-level='2'] {
|
||||
height: 40px;
|
||||
}
|
||||
.bn-side-menu[data-block-type=heading][data-level="3"] {
|
||||
.bn-side-menu[data-block-type='heading'][data-level='3'] {
|
||||
height: 40px;
|
||||
}
|
||||
& .bn-editor h1 {
|
||||
@@ -55,7 +77,7 @@ const cssEditor = (readonly: boolean) => `
|
||||
& .bn-editor h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.bn-block-content[data-is-empty-and-focused][data-content-type="paragraph"]
|
||||
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
|
||||
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import { Alert, Loader, VariantType } from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Card, Text, TextErrors } from '@/components';
|
||||
import { Box, Text, TextErrors } from '@/components';
|
||||
import { useCollaborationUrl } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { DocHeader } from '@/features/docs/doc-header';
|
||||
import { Doc, useDocStore } from '@/features/docs/doc-management';
|
||||
import { TableContent } from '@/features/docs/doc-table-content/';
|
||||
import { Versions, useDocVersion } from '@/features/docs/doc-versioning/';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocVersionHeader } from '../../doc-header/components/DocVersionHeader';
|
||||
import { useHeadingStore } from '../stores';
|
||||
|
||||
import { BlockNoteEditor } from './BlockNoteEditor';
|
||||
import { IconOpenPanelEditor, PanelEditor } from './PanelEditor';
|
||||
|
||||
interface DocEditorProps {
|
||||
doc: Doc;
|
||||
versionId?: Versions['version_id'];
|
||||
}
|
||||
|
||||
export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||
const {
|
||||
query: { versionId },
|
||||
} = useRouter();
|
||||
export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { headings } = useHeadingStore();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
|
||||
const isVersion = versionId && typeof versionId === 'string';
|
||||
const isVersion = !!versionId && typeof versionId === 'string';
|
||||
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
@@ -41,43 +41,49 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocHeader doc={doc} versionId={versionId as Versions['version_id']} />
|
||||
{!doc.abilities.partial_update && (
|
||||
<Box $margin={{ all: 'small', top: 'none' }}>
|
||||
<Alert type={VariantType.WARNING}>
|
||||
{t(`Read only, you cannot edit this document.`)}
|
||||
</Alert>
|
||||
{isDesktop && !isVersion && (
|
||||
<Box
|
||||
$position="absolute"
|
||||
$css={css`
|
||||
top: 72px;
|
||||
right: 20px;
|
||||
`}
|
||||
>
|
||||
<TableContent headings={headings} />
|
||||
</Box>
|
||||
)}
|
||||
{isVersion && (
|
||||
<Box $margin={{ all: 'small', top: 'none' }}>
|
||||
<Alert type={VariantType.WARNING}>
|
||||
{t(`Read only, you cannot edit document versions.`)}
|
||||
</Alert>
|
||||
<Box $maxWidth="868px" $width="100%" $height="100%">
|
||||
<Box $padding={{ horizontal: '54px' }}>
|
||||
{isVersion ? (
|
||||
<DocVersionHeader title={doc.title} />
|
||||
) : (
|
||||
<DocHeader doc={doc} />
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
$background={colorsTokens()['primary-bg']}
|
||||
$height="100%"
|
||||
$direction="row"
|
||||
$margin={{ all: isMobile ? 'tiny' : 'small', top: 'none' }}
|
||||
$css="overflow-x: clip;"
|
||||
$position="relative"
|
||||
>
|
||||
<Card
|
||||
$padding={isMobile ? 'small' : 'big'}
|
||||
$css="flex:1;"
|
||||
$overflow="auto"
|
||||
|
||||
{!doc.abilities.partial_update && (
|
||||
<Box $width="100%" $margin={{ all: 'small', top: 'none' }}>
|
||||
<Alert type={VariantType.WARNING}>
|
||||
{t(`Read only, you cannot edit this document.`)}
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
$background={colorsTokens()['primary-bg']}
|
||||
$direction="row"
|
||||
$width="100%"
|
||||
$css="overflow-x: clip; flex: 1;"
|
||||
$position="relative"
|
||||
>
|
||||
{isVersion ? (
|
||||
<DocVersionEditor doc={doc} versionId={versionId} />
|
||||
) : (
|
||||
<BlockNoteEditor doc={doc} storeId={doc.id} provider={provider} />
|
||||
)}
|
||||
{!isMobile && <IconOpenPanelEditor headings={headings} />}
|
||||
</Card>
|
||||
<PanelEditor doc={doc} headings={headings} />
|
||||
<Box $css="flex:1;" $overflow="auto" $position="relative">
|
||||
{isVersion ? (
|
||||
<DocVersionEditor doc={doc} versionId={versionId} />
|
||||
) : (
|
||||
<BlockNoteEditor doc={doc} storeId={doc.id} provider={provider} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
import React, { PropsWithChildren, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, BoxButton, Card, IconBG, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import { TableContent } from '@/features/docs/doc-table-content';
|
||||
import { VersionList } from '@/features/docs/doc-versioning';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { usePanelEditorStore } from '../stores';
|
||||
import { HeadingBlock } from '../types';
|
||||
|
||||
interface PanelProps {
|
||||
doc: Doc;
|
||||
headings: HeadingBlock[];
|
||||
}
|
||||
|
||||
export const PanelEditor = ({
|
||||
doc,
|
||||
headings,
|
||||
}: PropsWithChildren<PanelProps>) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
const { isPanelTableContentOpen, setIsPanelTableContentOpen, isPanelOpen } =
|
||||
usePanelEditorStore();
|
||||
|
||||
return (
|
||||
<Card
|
||||
$width="100%"
|
||||
$maxWidth="20rem"
|
||||
$position={isMobile ? 'absolute' : 'sticky'}
|
||||
$height="100%"
|
||||
$hasTransition="slow"
|
||||
$css={`
|
||||
top: 0vh;
|
||||
right: 0;
|
||||
transform: translateX(0%);
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
${
|
||||
!isPanelOpen &&
|
||||
`
|
||||
transform: translateX(200%);
|
||||
opacity: 0;
|
||||
flex: 0;
|
||||
margin-left: 0rem;
|
||||
max-width: 0rem;
|
||||
`
|
||||
}
|
||||
`}
|
||||
aria-label={t('Document panel')}
|
||||
aria-hidden={!isPanelOpen}
|
||||
>
|
||||
<Box
|
||||
$overflow="inherit"
|
||||
$position="sticky"
|
||||
$hasTransition="slow"
|
||||
$css={`
|
||||
top: 0;
|
||||
opacity: ${isPanelOpen ? '1' : '0'};
|
||||
`}
|
||||
$maxHeight="99vh"
|
||||
>
|
||||
{isMobile && <IconOpenPanelEditor headings={headings} />}
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$align="center"
|
||||
$position="relative"
|
||||
$background={colorsTokens()['primary-400']}
|
||||
$margin={{ bottom: 'tiny' }}
|
||||
$radius="4px 4px 0 0"
|
||||
>
|
||||
<Box
|
||||
$background="white"
|
||||
$position="absolute"
|
||||
$height="100%"
|
||||
$width={doc.abilities.versions_list ? '50%' : '100%'}
|
||||
$hasTransition="slow"
|
||||
$css={`
|
||||
border-top: 2px solid ${colorsTokens()['primary-600']};
|
||||
border-radius: 0 4px 0 0;
|
||||
${
|
||||
isPanelTableContentOpen
|
||||
? `
|
||||
transform: translateX(0);
|
||||
border-radius: 4px 0 0 0;
|
||||
`
|
||||
: `transform: translateX(100%);`
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<BoxButton
|
||||
$minWidth={doc.abilities.versions_list ? '50%' : '100%'}
|
||||
onClick={() => setIsPanelTableContentOpen(true)}
|
||||
$zIndex={1}
|
||||
>
|
||||
<Text
|
||||
$width="100%"
|
||||
$weight="bold"
|
||||
$size="m"
|
||||
$theme="primary"
|
||||
$variation="600"
|
||||
$padding={{ vertical: 'small', horizontal: 'small' }}
|
||||
>
|
||||
{t('Table of content')}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
{doc.abilities.versions_list && (
|
||||
<BoxButton
|
||||
$minWidth="50%"
|
||||
onClick={() => setIsPanelTableContentOpen(false)}
|
||||
$zIndex={1}
|
||||
>
|
||||
<Text
|
||||
$width="100%"
|
||||
$weight="bold"
|
||||
$size="m"
|
||||
$theme="primary"
|
||||
$variation="600"
|
||||
$padding={{ vertical: 'small', horizontal: 'small' }}
|
||||
>
|
||||
{t('Versions')}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
)}
|
||||
</Box>
|
||||
{isPanelTableContentOpen && <TableContent headings={headings} />}
|
||||
{!isPanelTableContentOpen && doc.abilities.versions_list && (
|
||||
<VersionList doc={doc} />
|
||||
)}
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
interface IconOpenPanelEditorProps {
|
||||
headings: HeadingBlock[];
|
||||
}
|
||||
|
||||
export const IconOpenPanelEditor = ({ headings }: IconOpenPanelEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { setIsPanelOpen, isPanelOpen, setIsPanelTableContentOpen } =
|
||||
usePanelEditorStore();
|
||||
const [hasBeenOpen, setHasBeenOpen] = useState(isPanelOpen);
|
||||
const { isMobile } = useResponsiveStore();
|
||||
|
||||
const setClosePanel = () => {
|
||||
setHasBeenOpen(true);
|
||||
setIsPanelOpen(!isPanelOpen);
|
||||
};
|
||||
|
||||
// Open the panel if there are more than 1 heading
|
||||
useEffect(() => {
|
||||
if (headings?.length && headings.length > 1 && !hasBeenOpen && !isMobile) {
|
||||
setIsPanelTableContentOpen(true);
|
||||
setIsPanelOpen(true);
|
||||
setHasBeenOpen(true);
|
||||
}
|
||||
}, [
|
||||
headings,
|
||||
setIsPanelTableContentOpen,
|
||||
setIsPanelOpen,
|
||||
hasBeenOpen,
|
||||
isMobile,
|
||||
]);
|
||||
|
||||
// If open from the doc header we set the state as well
|
||||
useEffect(() => {
|
||||
if (isPanelOpen && !hasBeenOpen) {
|
||||
setHasBeenOpen(true);
|
||||
}
|
||||
}, [hasBeenOpen, isPanelOpen]);
|
||||
|
||||
// Close the panel unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setIsPanelOpen(false);
|
||||
};
|
||||
}, [setIsPanelOpen]);
|
||||
|
||||
return (
|
||||
<IconBG
|
||||
iconName="menu_open"
|
||||
aria-label={isPanelOpen ? t('Close the panel') : t('Open the panel')}
|
||||
$background="transparent"
|
||||
$size="h2"
|
||||
$zIndex={10}
|
||||
$hasTransition="slow"
|
||||
$css={`
|
||||
cursor: pointer;
|
||||
right: 0rem;
|
||||
top: 0.1rem;
|
||||
transform: rotate(${isPanelOpen ? '180deg' : '0deg'});
|
||||
user-select: none;
|
||||
${hasBeenOpen ? 'display:flex;' : 'display: none;'}
|
||||
`}
|
||||
$position="absolute"
|
||||
onClick={setClosePanel}
|
||||
$radius="2px"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,107 +1,99 @@
|
||||
import { Fragment } from 'react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Card, StyledLink, Text } from '@/components';
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { HorizontalSeparator } from '@/components/separators/HorizontalSeparator';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, currentDocRole, useTrans } from '@/features/docs/doc-management';
|
||||
import { Versions } from '@/features/docs/doc-versioning';
|
||||
import { useDate } from '@/hook';
|
||||
import {
|
||||
Doc,
|
||||
LinkReach,
|
||||
currentDocRole,
|
||||
useTrans,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocTagPublic } from './DocTagPublic';
|
||||
import { DocTitle } from './DocTitle';
|
||||
import { DocToolBox } from './DocToolBox';
|
||||
|
||||
interface DocHeaderProps {
|
||||
doc: Doc;
|
||||
versionId?: Versions['version_id'];
|
||||
}
|
||||
|
||||
export const DocHeader = ({ doc, versionId }: DocHeaderProps) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
export const DocHeader = ({ doc }: DocHeaderProps) => {
|
||||
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const spacings = spacingsTokens();
|
||||
const colors = colorsTokens();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { formatDate } = useDate();
|
||||
const docIsPublic = doc.link_reach === LinkReach.PUBLIC;
|
||||
|
||||
const { transRole } = useTrans();
|
||||
const { isMobile, isSmallMobile } = useResponsiveStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
$margin={isMobile ? 'tiny' : 'small'}
|
||||
<Box
|
||||
$width="100%"
|
||||
$padding={{ top: 'base' }}
|
||||
$gap={spacings['base']}
|
||||
aria-label={t('It is the card information about the document.')}
|
||||
>
|
||||
<Box
|
||||
$padding={isMobile ? 'tiny' : 'small'}
|
||||
$direction="row"
|
||||
$align="center"
|
||||
>
|
||||
<StyledLink href="/">
|
||||
<Text
|
||||
$isMaterialIcon
|
||||
$theme="primary"
|
||||
$variation="600"
|
||||
$size="2rem"
|
||||
$css={css`
|
||||
&:hover {
|
||||
background-color: ${colorsTokens()['primary-100']};
|
||||
}
|
||||
`}
|
||||
$hasTransition
|
||||
$radius="5px"
|
||||
$padding="tiny"
|
||||
>
|
||||
home
|
||||
</Text>
|
||||
</StyledLink>
|
||||
{docIsPublic && (
|
||||
<Box
|
||||
$width="1px"
|
||||
$height="70%"
|
||||
$background={colorsTokens()['greyscale-100']}
|
||||
$margin={{ horizontal: 'tiny' }}
|
||||
/>
|
||||
aria-label={t('Public document')}
|
||||
$color={colors['primary-600']}
|
||||
$background={colors['primary-100']}
|
||||
$radius={spacings['3xs']}
|
||||
$direction="row"
|
||||
$padding="xs"
|
||||
$flex={1}
|
||||
$align="center"
|
||||
$gap={spacings['3xs']}
|
||||
$css={css`
|
||||
border: 1px solid var(--c--theme--colors--primary-300, #e3e3fd);
|
||||
`}
|
||||
>
|
||||
<Icon data-testid="public-icon" iconName="public" />
|
||||
<Text>{t('Public document')}</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box $direction="row" $align="center" $width="100%">
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$css="flex:1;"
|
||||
$gap="0.5rem 1rem"
|
||||
$wrap="wrap"
|
||||
$align="center"
|
||||
>
|
||||
<DocTitle doc={doc} />
|
||||
<DocToolBox doc={doc} versionId={versionId} />
|
||||
<Box $gap={spacings['3xs']}>
|
||||
<DocTitle doc={doc} />
|
||||
|
||||
<Box $direction="row">
|
||||
{isDesktop && (
|
||||
<>
|
||||
<Text $variation="600" $size="s" $weight="bold">
|
||||
{transRole(currentDocRole(doc.abilities))} ·
|
||||
</Text>
|
||||
<Text $variation="600" $size="s">
|
||||
{t('Last update: {{update}}', {
|
||||
update: DateTime.fromISO(doc.updated_at).toRelative(),
|
||||
})}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{!isDesktop && (
|
||||
<Text $variation="400" $size="s">
|
||||
{DateTime.fromISO(doc.updated_at).toRelative()}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<DocToolBox doc={doc} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
$direction={isSmallMobile ? 'column' : 'row'}
|
||||
$align={isSmallMobile ? 'start' : 'center'}
|
||||
$css="border-top:1px solid #eee"
|
||||
$padding={{
|
||||
horizontal: isMobile ? 'tiny' : 'big',
|
||||
vertical: 'tiny',
|
||||
}}
|
||||
$gap="0.5rem 2rem"
|
||||
$justify="space-between"
|
||||
$wrap="wrap"
|
||||
$position="relative"
|
||||
>
|
||||
<Box
|
||||
$direction={isSmallMobile ? 'column' : 'row'}
|
||||
$align={isSmallMobile ? 'start' : 'center'}
|
||||
$gap="0.5rem 2rem"
|
||||
$wrap="wrap"
|
||||
>
|
||||
<DocTagPublic doc={doc} />
|
||||
<Text $size="s" $display="inline">
|
||||
{t('Created at')} <strong>{formatDate(doc.created_at)}</strong>
|
||||
</Text>
|
||||
</Box>
|
||||
<Text $size="s" $display="inline">
|
||||
{t('Your role:')}{' '}
|
||||
<strong>{transRole(currentDocRole(doc.abilities))}</strong>
|
||||
</Text>
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
<HorizontalSeparator $withPadding={true} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useHeadingStore } from '@/features/docs/doc-editor';
|
||||
import {
|
||||
Doc,
|
||||
KEY_DOC,
|
||||
@@ -19,45 +19,50 @@ import {
|
||||
useUpdateDoc,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { useBroadcastStore, useResponsiveStore } from '@/stores';
|
||||
import { isFirefox } from '@/utils/userAgent';
|
||||
|
||||
interface DocTitleProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const DocTitle = ({ doc }: DocTitleProps) => {
|
||||
const { isMobile } = useResponsiveStore();
|
||||
|
||||
if (!doc.abilities.partial_update) {
|
||||
return (
|
||||
<Text
|
||||
as="h2"
|
||||
$margin={{ all: 'none', left: 'tiny' }}
|
||||
$size={isMobile ? 'h4' : 'h2'}
|
||||
>
|
||||
{doc.title}
|
||||
</Text>
|
||||
);
|
||||
return <DocTitleText title={doc.title} />;
|
||||
}
|
||||
|
||||
return <DocTitleInput doc={doc} />;
|
||||
};
|
||||
|
||||
interface DocTitleTextProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const DocTitleText = ({ title }: DocTitleTextProps) => {
|
||||
const { isMobile } = useResponsiveStore();
|
||||
return (
|
||||
<Text
|
||||
as="h2"
|
||||
$margin={{ all: 'none', left: 'none' }}
|
||||
$size={isMobile ? 'h4' : 'h2'}
|
||||
$variation="1000"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const [titleDisplay, setTitleDisplay] = useState(doc.title);
|
||||
const { toast } = useToastProvider();
|
||||
const { untitledDocument } = useTrans();
|
||||
const isUntitled = titleDisplay === untitledDocument;
|
||||
const { headings } = useHeadingStore();
|
||||
const headingText = headings?.[0]?.contentText;
|
||||
const debounceRef = useRef<NodeJS.Timeout>();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
|
||||
const { broadcast } = useBroadcastStore();
|
||||
|
||||
const { mutate: updateDoc } = useUpdateDoc({
|
||||
listInvalideQueries: [KEY_LIST_DOC],
|
||||
listInvalideQueries: [KEY_DOC, KEY_LIST_DOC],
|
||||
onSuccess(data) {
|
||||
if (data.title !== untitledDocument) {
|
||||
toast(t('Document title updated successfully'), VariantType.SUCCESS);
|
||||
@@ -81,10 +86,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
|
||||
// If mutation we update
|
||||
if (sanitizedTitle !== doc.title) {
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
debounceRef.current = undefined;
|
||||
}
|
||||
setTitleDisplay(sanitizedTitle);
|
||||
updateDoc({ id: doc.id, title: sanitizedTitle });
|
||||
}
|
||||
},
|
||||
@@ -98,74 +100,38 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnClick = () => {
|
||||
if (isUntitled) {
|
||||
setTitleDisplay('');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTitleDisplay(doc.title);
|
||||
}, [doc.title]);
|
||||
|
||||
useEffect(() => {
|
||||
if ((!debounceRef.current && !isUntitled) || !headingText) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTitleDisplay(headingText);
|
||||
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
|
||||
debounceRef.current = setTimeout(() => {
|
||||
handleTitleSubmit(headingText);
|
||||
debounceRef.current = undefined;
|
||||
}, 3000);
|
||||
}, [isUntitled, handleTitleSubmit, headingText]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip content={t('Rename')} placement="top">
|
||||
<Box
|
||||
as="h2"
|
||||
$radius="4px"
|
||||
$padding={{ horizontal: 'tiny', vertical: '4px' }}
|
||||
$margin="none"
|
||||
$minWidth="200px"
|
||||
contentEditable={isFirefox() ? 'true' : 'plaintext-only'}
|
||||
onClick={handleOnClick}
|
||||
onBlurCapture={(e) =>
|
||||
handleTitleSubmit(e.currentTarget.textContent || '')
|
||||
}
|
||||
as="span"
|
||||
role="textbox"
|
||||
contentEditable
|
||||
defaultValue={isUntitled ? undefined : titleDisplay}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
suppressContentEditableWarning={true}
|
||||
$color={
|
||||
isUntitled
|
||||
? colorsTokens()['greyscale-200']
|
||||
: colorsTokens()['greyscale-text']
|
||||
aria-label="doc title input"
|
||||
onBlurCapture={(event) =>
|
||||
handleTitleSubmit(event.target.textContent || '')
|
||||
}
|
||||
$css={`
|
||||
${isUntitled && 'font-style: italic;'}
|
||||
cursor: text;
|
||||
font-size: ${isMobile ? '1.2rem' : '1.5rem'};
|
||||
transition: box-shadow 0.5s, border-color 0.5s;
|
||||
border: 1px dashed transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(0, 123, 255, 0.25);
|
||||
border-style: dashed;
|
||||
$color={colorsTokens()['greyscale-1000']}
|
||||
$margin={{ left: '-2px', right: '10px' }}
|
||||
$css={css`
|
||||
&[contenteditable='true']:empty:not(:focus):before {
|
||||
content: '${untitledDocument}';
|
||||
color: grey;
|
||||
pointer-events: none;
|
||||
font-style: italic;
|
||||
}
|
||||
font-size: ${isDesktop
|
||||
? css`var(--c--theme--font--sizes--h2)`
|
||||
: css`var(--c--theme--font--sizes--sm)`};
|
||||
font-weight: 700;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: transparent;
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
outline: none;
|
||||
`}
|
||||
>
|
||||
{titleDisplay}
|
||||
{isUntitled ? '' : titleDisplay}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</>
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import {
|
||||
Button,
|
||||
VariantType,
|
||||
useModal,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, DropButton, IconOptions } from '@/components';
|
||||
import {
|
||||
Box,
|
||||
DropdownMenu,
|
||||
DropdownMenuOption,
|
||||
Icon,
|
||||
IconOptions,
|
||||
} from '@/components';
|
||||
import { useAuthStore } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import {
|
||||
useEditorStore,
|
||||
usePanelEditorStore,
|
||||
@@ -17,29 +26,94 @@ import {
|
||||
ModalRemoveDoc,
|
||||
ModalShare,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { ModalVersion, Versions } from '@/features/docs/doc-versioning';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { ModalSelectVersion } from '../../doc-versioning/components/ModalSelectVersion';
|
||||
|
||||
import { ModalPDF } from './ModalExport';
|
||||
|
||||
interface DocToolBoxProps {
|
||||
doc: Doc;
|
||||
versionId?: Versions['version_id'];
|
||||
}
|
||||
|
||||
export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => {
|
||||
export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const spacings = spacingsTokens();
|
||||
const colors = colorsTokens();
|
||||
|
||||
const [isModalShareOpen, setIsModalShareOpen] = useState(false);
|
||||
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
|
||||
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
|
||||
const [isDropOpen, setIsDropOpen] = useState(false);
|
||||
const selectHistoryModal = useModal();
|
||||
const { setIsPanelOpen, setIsPanelTableContentOpen } = usePanelEditorStore();
|
||||
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
|
||||
const { isSmallMobile } = useResponsiveStore();
|
||||
|
||||
const { isSmallMobile, isDesktop } = useResponsiveStore();
|
||||
const { authenticated } = useAuthStore();
|
||||
const { editor } = useEditorStore();
|
||||
const { toast } = useToastProvider();
|
||||
|
||||
const options: DropdownMenuOption[] = [
|
||||
...(isSmallMobile
|
||||
? [
|
||||
{
|
||||
label: t('Share'),
|
||||
icon: 'upload',
|
||||
callback: () => {
|
||||
setIsModalShareOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('Export'),
|
||||
icon: 'download',
|
||||
callback: () => {
|
||||
setIsModalPDFOpen(true);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: t('Version history'),
|
||||
icon: 'history',
|
||||
disabled: !doc.abilities.versions_list,
|
||||
callback: () => {
|
||||
selectHistoryModal.open();
|
||||
},
|
||||
show: isDesktop,
|
||||
},
|
||||
{
|
||||
label: t('Table of contents'),
|
||||
icon: 'summarize',
|
||||
callback: () => {
|
||||
setIsPanelOpen(true);
|
||||
setIsPanelTableContentOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('Copy as {{format}}', { format: 'Markdown' }),
|
||||
icon: 'content_copy',
|
||||
callback: () => {
|
||||
void copyCurrentEditorToClipboard('markdown');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('Copy as {{format}}', { format: 'HTML' }),
|
||||
icon: 'content_copy',
|
||||
callback: () => {
|
||||
void copyCurrentEditorToClipboard('html');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('Delete document'),
|
||||
icon: 'delete',
|
||||
disabled: !doc.abilities.destroy,
|
||||
callback: () => {
|
||||
setIsModalRemoveOpen(true);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const copyCurrentEditorToClipboard = async (
|
||||
asFormat: 'html' | 'markdown',
|
||||
) => {
|
||||
@@ -71,22 +145,10 @@ export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => {
|
||||
$gap="0.5rem 1.5rem"
|
||||
$wrap={isSmallMobile ? 'wrap' : 'nowrap'}
|
||||
>
|
||||
{versionId && (
|
||||
<Box $margin={{ left: 'auto' }}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsModalVersionOpen(true);
|
||||
}}
|
||||
color="secondary"
|
||||
size={isSmallMobile ? 'small' : 'medium'}
|
||||
>
|
||||
{t('Restore this version')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Box $direction="row" $margin={{ left: 'auto' }} $gap="1rem">
|
||||
{authenticated && (
|
||||
<Box $direction="row" $margin={{ left: 'auto' }} $gap={spacings['2xs']}>
|
||||
{authenticated && !isSmallMobile && (
|
||||
<Button
|
||||
color="tertiary-text"
|
||||
onClick={() => {
|
||||
setIsModalShareOpen(true);
|
||||
}}
|
||||
@@ -95,91 +157,38 @@ export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => {
|
||||
{t('Share')}
|
||||
</Button>
|
||||
)}
|
||||
<DropButton
|
||||
button={
|
||||
<IconOptions
|
||||
isOpen={isDropOpen}
|
||||
aria-label={t('Open the document options')}
|
||||
/>
|
||||
}
|
||||
onOpenChange={(isOpen) => setIsDropOpen(isOpen)}
|
||||
isOpen={isDropOpen}
|
||||
>
|
||||
<Box>
|
||||
{doc.abilities.versions_list && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsPanelOpen(true);
|
||||
setIsPanelTableContentOpen(false);
|
||||
setIsDropOpen(false);
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">history</span>}
|
||||
size="small"
|
||||
>
|
||||
{t('Version history')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsPanelOpen(true);
|
||||
setIsPanelTableContentOpen(true);
|
||||
setIsDropOpen(false);
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">summarize</span>}
|
||||
size="small"
|
||||
>
|
||||
{t('Table of contents')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsModalPDFOpen(true);
|
||||
setIsDropOpen(false);
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">file_download</span>}
|
||||
size="small"
|
||||
>
|
||||
{t('Export')}
|
||||
</Button>
|
||||
{doc.abilities.destroy && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsModalRemoveOpen(true);
|
||||
setIsDropOpen(false);
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">delete</span>}
|
||||
size="small"
|
||||
>
|
||||
{t('Delete document')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsDropOpen(false);
|
||||
void copyCurrentEditorToClipboard('markdown');
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">content_copy</span>}
|
||||
size="small"
|
||||
>
|
||||
{t('Copy as {{format}}', { format: 'Markdown' })}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsDropOpen(false);
|
||||
void copyCurrentEditorToClipboard('html');
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">content_copy</span>}
|
||||
size="small"
|
||||
>
|
||||
{t('Copy as {{format}}', { format: 'HTML' })}
|
||||
</Button>
|
||||
</Box>
|
||||
</DropButton>
|
||||
{!isSmallMobile && (
|
||||
<Button
|
||||
color="tertiary-text"
|
||||
icon={
|
||||
<Icon iconName="download" $theme="primary" $variation="800" />
|
||||
}
|
||||
onClick={() => {
|
||||
setIsModalPDFOpen(true);
|
||||
}}
|
||||
size={isSmallMobile ? 'small' : 'medium'}
|
||||
/>
|
||||
)}
|
||||
<DropdownMenu options={options}>
|
||||
<IconOptions
|
||||
isHorizontal
|
||||
$theme="primary"
|
||||
$radius={spacings['3xs']}
|
||||
$padding={{ all: 'xs' }}
|
||||
$css={css`
|
||||
&:hover {
|
||||
background-color: ${colors['greyscale-100']};
|
||||
}
|
||||
${isSmallMobile
|
||||
? css`
|
||||
padding: 10px;
|
||||
border: 1px solid ${colors['greyscale-300']};
|
||||
`
|
||||
: ''}
|
||||
`}
|
||||
aria-label={t('Open the document options')}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</Box>
|
||||
{isModalShareOpen && (
|
||||
<ModalShare onClose={() => setIsModalShareOpen(false)} doc={doc} />
|
||||
@@ -190,11 +199,10 @@ export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => {
|
||||
{isModalRemoveOpen && (
|
||||
<ModalRemoveDoc onClose={() => setIsModalRemoveOpen(false)} doc={doc} />
|
||||
)}
|
||||
{isModalVersionOpen && versionId && (
|
||||
<ModalVersion
|
||||
onClose={() => setIsModalVersionOpen(false)}
|
||||
docId={doc.id}
|
||||
versionId={versionId}
|
||||
{selectHistoryModal.isOpen && (
|
||||
<ModalSelectVersion
|
||||
onClose={() => selectHistoryModal.close()}
|
||||
doc={doc}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { HorizontalSeparator } from '@/components/separators/HorizontalSeparator';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { DocTitleText } from './DocTitle';
|
||||
|
||||
interface DocVersionHeaderProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const DocVersionHeader = ({ title }: DocVersionHeaderProps) => {
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
|
||||
const spacings = spacingsTokens();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
$width="100%"
|
||||
$padding={{ vertical: 'base' }}
|
||||
$gap={spacings['base']}
|
||||
aria-label={t('It is the document title')}
|
||||
>
|
||||
<DocTitleText title={title} />
|
||||
<HorizontalSeparator />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,8 @@
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Loader,
|
||||
Modal,
|
||||
ModalSize,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Select,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
@@ -21,6 +18,11 @@ import { useExport } from '../api/useExport';
|
||||
import { TemplatesOrdering, useTemplates } from '../api/useTemplates';
|
||||
import { adaptBlockNoteHTML, downloadFile } from '../utils';
|
||||
|
||||
export enum DocDownloadFormat {
|
||||
PDF = 'pdf',
|
||||
DOCX = 'docx',
|
||||
}
|
||||
|
||||
interface ModalPDFProps {
|
||||
onClose: () => void;
|
||||
doc: Doc;
|
||||
@@ -41,7 +43,9 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
|
||||
error,
|
||||
} = useExport();
|
||||
const [templateIdSelected, setTemplateIdSelected] = useState<string>();
|
||||
const [format, setFormat] = useState<'pdf' | 'docx'>('pdf');
|
||||
const [format, setFormat] = useState<DocDownloadFormat>(
|
||||
DocDownloadFormat.PDF,
|
||||
);
|
||||
|
||||
const templateOptions = useMemo(() => {
|
||||
if (!templates?.pages) {
|
||||
@@ -123,46 +127,37 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
data-testid="modal-export"
|
||||
isOpen
|
||||
closeOnClickOutside
|
||||
hideCloseButton
|
||||
leftActions={
|
||||
<Button
|
||||
aria-label={t('Close the modal')}
|
||||
color="secondary"
|
||||
fullWidth
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
}
|
||||
onClose={() => onClose()}
|
||||
rightActions={
|
||||
<Button
|
||||
aria-label={t('Download')}
|
||||
color="primary"
|
||||
fullWidth
|
||||
onClick={() => void onSubmit()}
|
||||
disabled={isPending || !templateIdSelected}
|
||||
>
|
||||
{t('Download')}
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
aria-label={t('Close the modal')}
|
||||
color="secondary"
|
||||
fullWidth
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Download')}
|
||||
color="primary"
|
||||
fullWidth
|
||||
onClick={() => void onSubmit()}
|
||||
disabled={isPending || !templateIdSelected}
|
||||
>
|
||||
{t('Download')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
size={ModalSize.MEDIUM}
|
||||
title={
|
||||
<Box $align="center" $gap="1rem">
|
||||
<Text
|
||||
className="material-icons"
|
||||
$size="3.5rem"
|
||||
$theme="primary"
|
||||
$variation="600"
|
||||
>
|
||||
picture_as_pdf
|
||||
</Text>
|
||||
<Text as="h2" $size="h3" $margin="none" $theme="primary">
|
||||
{t('Export')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text $size="h6" $align="flex-start">
|
||||
{t('Download')}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
@@ -170,14 +165,11 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
|
||||
aria-label={t('Content modal to export the document')}
|
||||
$gap="1.5rem"
|
||||
>
|
||||
<Alert canClose={false} type={VariantType.INFO}>
|
||||
<Text>
|
||||
{t(
|
||||
'Export your document, it will be inserted in the selected template.',
|
||||
)}
|
||||
</Text>
|
||||
</Alert>
|
||||
|
||||
<Text $variation="600">
|
||||
{t(
|
||||
'Upload your docs to a Microsoft Word, Open Office or PDF document.',
|
||||
)}
|
||||
</Text>
|
||||
<Select
|
||||
clearable={false}
|
||||
label={t('Template')}
|
||||
@@ -187,22 +179,19 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
|
||||
setTemplateIdSelected(options.target.value as string)
|
||||
}
|
||||
/>
|
||||
|
||||
<RadioGroup>
|
||||
<Radio
|
||||
label={t('PDF')}
|
||||
value="pdf"
|
||||
name="format"
|
||||
onChange={(evt) => setFormat(evt.target.value as 'pdf')}
|
||||
defaultChecked={true}
|
||||
/>
|
||||
<Radio
|
||||
label={t('Docx')}
|
||||
value="docx"
|
||||
name="format"
|
||||
onChange={(evt) => setFormat(evt.target.value as 'docx')}
|
||||
/>
|
||||
</RadioGroup>
|
||||
<Select
|
||||
clearable={false}
|
||||
fullWidth
|
||||
label={t('Format')}
|
||||
options={[
|
||||
{ label: t('Word / Open Office'), value: DocDownloadFormat.DOCX },
|
||||
{ label: t('PDF'), value: DocDownloadFormat.PDF },
|
||||
]}
|
||||
value={format}
|
||||
onChange={(options) =>
|
||||
setFormat(options.target.value as DocDownloadFormat)
|
||||
}
|
||||
/>
|
||||
|
||||
{isPending && (
|
||||
<Box $align="center" $margin={{ top: 'big' }}>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
|
||||
import {
|
||||
APIError,
|
||||
APIList,
|
||||
errorCauses,
|
||||
fetchAPI,
|
||||
useAPIInfiniteQuery,
|
||||
} from '@/api';
|
||||
|
||||
import { Doc } from '../types';
|
||||
|
||||
@@ -22,16 +28,24 @@ export type DocsOrdering = (typeof docsOrdering)[number];
|
||||
export type DocsParams = {
|
||||
page: number;
|
||||
ordering?: DocsOrdering;
|
||||
is_creator_me?: boolean;
|
||||
};
|
||||
|
||||
export type DocsResponse = APIList<Doc>;
|
||||
export const getDocs = async (params: DocsParams): Promise<DocsResponse> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params.page) {
|
||||
searchParams.set('page', params.page.toString());
|
||||
}
|
||||
|
||||
export const getDocs = async ({
|
||||
ordering,
|
||||
page,
|
||||
}: DocsParams): Promise<DocsResponse> => {
|
||||
const orderingQuery = ordering ? `&ordering=${ordering}` : '';
|
||||
const response = await fetchAPI(`documents/?page=${page}${orderingQuery}`);
|
||||
if (params.ordering) {
|
||||
searchParams.set('ordering', params.ordering);
|
||||
}
|
||||
if (params.is_creator_me !== undefined) {
|
||||
searchParams.set('is_creator_me', params.is_creator_me.toString());
|
||||
}
|
||||
|
||||
const response = await fetchAPI(`documents/?${searchParams.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError('Failed to get the docs', await errorCauses(response));
|
||||
@@ -52,3 +66,7 @@ export function useDocs(
|
||||
...queryConfig,
|
||||
});
|
||||
}
|
||||
|
||||
export const useInfiniteDocs = (params: DocsParams) => {
|
||||
return useAPIInfiniteQuery(KEY_LIST_DOC, getDocs, params);
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ export const useRemoveDoc = (options?: UseRemoveDocOptions) => {
|
||||
mutationFn: removeDoc,
|
||||
...options,
|
||||
onSuccess: (data, variables, context) => {
|
||||
void queryClient.resetQueries({
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_LIST_DOC],
|
||||
});
|
||||
if (options?.onSuccess) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<svg width="32" height="36" viewBox="0 0 32 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2.01394" y="1.23611" width="25.9722" height="33.5278" rx="3.54167" fill="white"/>
|
||||
<rect x="2.01394" y="1.23611" width="25.9722" height="33.5278" rx="3.54167" stroke="#DCDCFC" stroke-width="0.472222"/>
|
||||
<path d="M6.5 8.55556H15" stroke="#6A6AF4" stroke-width="1.88889" stroke-linecap="round"/>
|
||||
<path d="M6.5 11.3889H23.5M6.5 14.2222H23.5M6.5 17.0556H23.5M6.5 19.8889H23.5M6.5 22.7222H20.6667" stroke="#CACAFB" stroke-width="1.88889" stroke-linecap="round"/>
|
||||
<rect x="7" y="10" width="16" height="16" rx="8" fill="#6A6AF4"/>
|
||||
<rect x="7" y="10" width="16" height="16" rx="8" stroke="white" stroke-width="1.5"/>
|
||||
<path d="M16.8 18L18 19.2V20.1H15.45V22.95L15 23.4L14.55 22.95V20.1H12V19.2L13.2 18V14.7H12.6V13.8H17.4V14.7H16.8V18Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 853 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="28" height="34" viewBox="0 0 28 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1.01394" y="0.236111" width="25.9722" height="33.5278" rx="3.54167" fill="white"/>
|
||||
<rect x="1.01394" y="0.236111" width="25.9722" height="33.5278" rx="3.54167" stroke="#DCDCFC" stroke-width="0.472222"/>
|
||||
<path d="M5.5 7.55554H14" stroke="#6A6AF4" stroke-width="1.88889" stroke-linecap="round"/>
|
||||
<path d="M5.5 10.3889H22.5M5.5 13.2222H22.5M5.5 16.0556H22.5M5.5 18.8889H22.5M5.5 21.7222H22.5M5.5 24.5556H22.5M5.5 27.3889H22.5M5.5 30.2222H22.5M5.5 33.0556H22.5" stroke="#CACAFB" stroke-width="1.88889" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 635 B |
@@ -1,19 +1,17 @@
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Modal,
|
||||
ModalSize,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { t } from 'i18next';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Text, TextErrors } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham/';
|
||||
|
||||
import { useRemoveDoc } from '../api/useRemoveDoc';
|
||||
import IconDoc from '../assets/icon-doc.svg';
|
||||
import { Doc } from '../types';
|
||||
|
||||
interface ModalRemoveDocProps {
|
||||
@@ -22,13 +20,13 @@ interface ModalRemoveDocProps {
|
||||
}
|
||||
|
||||
export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { toast } = useToastProvider();
|
||||
const { push } = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
mutate: removeDoc,
|
||||
|
||||
isError,
|
||||
error,
|
||||
} = useRemoveDoc({
|
||||
@@ -36,7 +34,11 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
|
||||
toast(t('The document has been deleted.'), VariantType.SUCCESS, {
|
||||
duration: 4000,
|
||||
});
|
||||
void push('/');
|
||||
if (pathname === '/') {
|
||||
onClose();
|
||||
} else {
|
||||
void push('/');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -44,42 +46,36 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
|
||||
<Modal
|
||||
isOpen
|
||||
closeOnClickOutside
|
||||
hideCloseButton
|
||||
leftActions={
|
||||
<Button
|
||||
aria-label={t('Close the modal')}
|
||||
color="secondary"
|
||||
fullWidth
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
}
|
||||
onClose={() => onClose()}
|
||||
rightActions={
|
||||
<Button
|
||||
aria-label={t('Confirm deletion')}
|
||||
color="primary"
|
||||
fullWidth
|
||||
onClick={() =>
|
||||
removeDoc({
|
||||
docId: doc.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('Confirm deletion')}
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
aria-label={t('Close the modal')}
|
||||
color="secondary"
|
||||
fullWidth
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Confirm deletion')}
|
||||
color="danger"
|
||||
fullWidth
|
||||
onClick={() =>
|
||||
removeDoc({
|
||||
docId: doc.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('Delete')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
size={ModalSize.MEDIUM}
|
||||
size={ModalSize.SMALL}
|
||||
title={
|
||||
<Box $align="center" $gap="1rem">
|
||||
<Text $isMaterialIcon $size="48px" $theme="primary" $variation="600">
|
||||
delete_forever
|
||||
</Text>
|
||||
<Text as="h2" $size="h3" $margin="none">
|
||||
{t('Deleting the document "{{title}}"', { title: doc.title })}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text $size="h6" $align="flex-start">
|
||||
{t('Delete a doc')}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
@@ -87,42 +83,14 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
|
||||
aria-label={t('Content modal to delete document')}
|
||||
>
|
||||
{!isError && (
|
||||
<Alert canClose={false} type={VariantType.WARNING}>
|
||||
<Text>
|
||||
{t('Are you sure you want to delete the document "{{title}}"?', {
|
||||
title: doc.title,
|
||||
})}
|
||||
</Text>
|
||||
</Alert>
|
||||
<Text $size="sm" $variation="600">
|
||||
{t('Are you sure you want to delete the document "{{title}}"?', {
|
||||
title: doc.title,
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{isError && <TextErrors causes={error.cause} />}
|
||||
|
||||
<Text
|
||||
as="p"
|
||||
$padding="small"
|
||||
$direction="row"
|
||||
$gap="0.5rem"
|
||||
$background={colorsTokens()['primary-150']}
|
||||
$theme="primary"
|
||||
$align="center"
|
||||
$radius="2px"
|
||||
>
|
||||
<IconDoc
|
||||
className="p-t"
|
||||
aria-label={t(`Document icon`)}
|
||||
color={colorsTokens()['primary-500']}
|
||||
width={58}
|
||||
style={{
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#ffffff',
|
||||
border: `1px solid ${colorsTokens()['primary-300']}`,
|
||||
}}
|
||||
/>
|
||||
<Text $theme="primary" $weight="bold" $size="l">
|
||||
{doc.title}
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -31,7 +31,7 @@ export const useDocStore = create<UseDocStore>((set, get) => ({
|
||||
} else {
|
||||
const initialDocContent = [
|
||||
{
|
||||
type: 'heading',
|
||||
type: 'paragraph',
|
||||
content: '',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -59,3 +59,9 @@ export interface Doc {
|
||||
versions_retrieve: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export enum DocDefaultFilter {
|
||||
ALL_DOCS = 'all_docs',
|
||||
MY_DOCS = 'my_docs',
|
||||
SHARED_WITH_ME = 'shared_with_me',
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import { BoxButton, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
const sizeMap: { [key: number]: string } = {
|
||||
1: '1.1rem',
|
||||
const leftPaddingMap: { [key: number]: string } = {
|
||||
3: '1.5rem',
|
||||
2: '0.9rem',
|
||||
3: '0.8rem',
|
||||
1: '0.3',
|
||||
};
|
||||
|
||||
export type HeadingsHighlight = {
|
||||
@@ -34,9 +34,12 @@ export const Heading = ({
|
||||
const [isHover, setIsHover] = useState(isHighlight);
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
const isActive = isHighlight || isHover;
|
||||
|
||||
return (
|
||||
<BoxButton
|
||||
id={`heading-${headingId}`}
|
||||
$width="100%"
|
||||
key={headingId}
|
||||
onMouseOver={() => setIsHover(true)}
|
||||
onMouseLeave={() => setIsHover(false)}
|
||||
@@ -47,23 +50,24 @@ export const Heading = ({
|
||||
}
|
||||
|
||||
editor.setTextCursorPosition(headingId, 'end');
|
||||
|
||||
document.querySelector(`[data-id="${headingId}"]`)?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
inline: 'start',
|
||||
block: 'start',
|
||||
});
|
||||
}}
|
||||
$radius="4px"
|
||||
$background={isActive ? `${colorsTokens()['greyscale-100']}` : 'none'}
|
||||
$css="text-align: left;"
|
||||
>
|
||||
<Text
|
||||
$theme="primary"
|
||||
$padding={{ vertical: 'xtiny', left: 'tiny' }}
|
||||
$size={sizeMap[level]}
|
||||
$width="100%"
|
||||
$padding={{ vertical: 'xtiny', left: leftPaddingMap[level] }}
|
||||
$variation={isActive ? '1000' : '700'}
|
||||
$weight={isActive ? 'bold' : 'normal'}
|
||||
$css="overflow-wrap: break-word;"
|
||||
$hasTransition
|
||||
$css={
|
||||
isHover || isHighlight
|
||||
? `box-shadow: -2px 0px 0px ${colorsTokens()[isHighlight ? 'primary-500' : 'primary-400']};`
|
||||
: ''
|
||||
}
|
||||
aria-selected={isHighlight}
|
||||
>
|
||||
{text}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, Text } from '@/components';
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { HeadingBlock, useEditorStore } from '@/features/docs/doc-editor';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
|
||||
|
||||
import { Heading } from './Heading';
|
||||
|
||||
interface TableContentProps {
|
||||
type TableContentProps = {
|
||||
headings: HeadingBlock[];
|
||||
}
|
||||
};
|
||||
|
||||
export const TableContent = ({ headings }: TableContentProps) => {
|
||||
const { editor } = useEditorStore();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [headingIdHighlight, setHeadingIdHighlight] = useState<string>();
|
||||
|
||||
// To highlight the first heading in the viewport
|
||||
const { t } = useTranslation();
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (!headings) {
|
||||
@@ -46,7 +48,7 @@ export const TableContent = ({ headings }: TableContentProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
document.getElementById(MAIN_LAYOUT_ID)?.addEventListener('scroll', () => {
|
||||
setTimeout(() => {
|
||||
handleScroll();
|
||||
}, 300);
|
||||
@@ -64,69 +66,84 @@ export const TableContent = ({ headings }: TableContentProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box $padding={{ all: 'small', right: 'none' }} $maxHeight="95%">
|
||||
<Box $overflow="auto" $padding={{ left: '2px' }}>
|
||||
{headings?.map(
|
||||
(heading) =>
|
||||
heading.contentText && (
|
||||
<Heading
|
||||
editor={editor}
|
||||
headingId={heading.id}
|
||||
level={heading.props.level}
|
||||
text={heading.contentText}
|
||||
key={heading.id}
|
||||
isHighlight={headingIdHighlight === heading.id}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
$height="1px"
|
||||
$width="auto"
|
||||
$background="#e5e5e5"
|
||||
$margin={{ vertical: 'small' }}
|
||||
$css="flex: none;"
|
||||
/>
|
||||
<BoxButton
|
||||
onClick={() => {
|
||||
// With mobile the focus open the keyboard and the scroll is not working
|
||||
if (!isMobile) {
|
||||
editor.focus();
|
||||
}
|
||||
<Box
|
||||
onMouseEnter={() => {
|
||||
setIsHover(true);
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(
|
||||
`heading-${headingIdHighlight}`,
|
||||
);
|
||||
|
||||
document.querySelector(`.bn-editor`)?.scrollIntoView({
|
||||
element?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
inline: 'center',
|
||||
block: 'center',
|
||||
});
|
||||
}}
|
||||
$align="start"
|
||||
>
|
||||
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
|
||||
{t('Back to top')}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
<BoxButton
|
||||
onClick={() => {
|
||||
// With mobile the focus open the keyboard and the scroll is not working
|
||||
if (!isMobile) {
|
||||
editor.focus();
|
||||
}
|
||||
}, 250); // 300ms is the transition time of the box
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHover(false);
|
||||
}}
|
||||
id="summaryContainer"
|
||||
$effect="show"
|
||||
$width="40px"
|
||||
$height="40px"
|
||||
$zIndex={1000}
|
||||
$align="center"
|
||||
$padding="xs"
|
||||
$justify="center"
|
||||
$css={css`
|
||||
border: 1px solid #ccc;
|
||||
overflow: hidden;
|
||||
border-radius: var(--c--theme--spacings--3xs);
|
||||
background: var(--c--theme--colors--greyscale-000);
|
||||
|
||||
document
|
||||
.querySelector(
|
||||
`.bn-editor > .bn-block-group > .bn-block-outer:last-child`,
|
||||
)
|
||||
?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
}}
|
||||
$align="start"
|
||||
>
|
||||
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
|
||||
{t('Go to bottom')}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
&:hover {
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
gap: var(--c--theme--spacings--2xs);
|
||||
width: 200px;
|
||||
height: auto;
|
||||
max-height: calc(50vh - 60px);
|
||||
}
|
||||
`}
|
||||
>
|
||||
{!isHover && (
|
||||
<Box $justify="center" $align="center">
|
||||
<Icon iconName="list" $theme="primary" $variation="800" />
|
||||
</Box>
|
||||
)}
|
||||
{isHover && (
|
||||
<Box $width="100%">
|
||||
<Box
|
||||
$margin={{ bottom: '20px' }}
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$align="center"
|
||||
>
|
||||
<Text $weight="bold" $variation="800" $theme="primary">
|
||||
{t('Summary')}
|
||||
</Text>
|
||||
<Icon iconName="list" $theme="primary" $variation="800" />
|
||||
</Box>
|
||||
{headings?.map(
|
||||
(heading) =>
|
||||
heading.contentText && (
|
||||
<Heading
|
||||
editor={editor}
|
||||
headingId={heading.id}
|
||||
level={heading.props.level}
|
||||
text={heading.contentText}
|
||||
key={heading.id}
|
||||
isHighlight={headingIdHighlight === heading.id}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './TableContent';
|
||||
export * from './Heading';
|
||||
|
||||
@@ -77,6 +77,7 @@ export function useDocVersionsInfiniteQuery(
|
||||
getNextPageParam(lastPage) {
|
||||
return lastPage.next_version_id_marker || undefined;
|
||||
},
|
||||
|
||||
...queryConfig,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Modal,
|
||||
ModalSize,
|
||||
@@ -18,18 +17,18 @@ import { KEY_LIST_DOC_VERSIONS } from '../api/useDocVersions';
|
||||
import { Versions } from '../types';
|
||||
import { revertUpdate } from '../utils';
|
||||
|
||||
interface ModalVersionProps {
|
||||
interface ModalConfirmationVersionProps {
|
||||
onClose: () => void;
|
||||
docId: Doc['id'];
|
||||
|
||||
versionId: Versions['version_id'];
|
||||
}
|
||||
|
||||
export const ModalVersion = ({
|
||||
export const ModalConfirmationVersion = ({
|
||||
onClose,
|
||||
docId,
|
||||
versionId,
|
||||
}: ModalVersionProps) => {
|
||||
}: ModalConfirmationVersionProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToastProvider();
|
||||
const { push } = useRouter();
|
||||
@@ -61,60 +60,50 @@ export const ModalVersion = ({
|
||||
<Modal
|
||||
isOpen
|
||||
closeOnClickOutside
|
||||
hideCloseButton
|
||||
leftActions={
|
||||
<Button
|
||||
aria-label={t('Close the modal')}
|
||||
color="secondary"
|
||||
fullWidth
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
}
|
||||
onClose={() => onClose()}
|
||||
rightActions={
|
||||
<Button
|
||||
aria-label={t('Restore')}
|
||||
color="primary"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
const newDoc = toBase64(
|
||||
Y.encodeStateAsUpdate(providers?.[versionId].document),
|
||||
);
|
||||
<>
|
||||
<Button
|
||||
aria-label={t('Close the modal')}
|
||||
color="secondary"
|
||||
fullWidth
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Restore')}
|
||||
color="danger"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
const newDoc = toBase64(
|
||||
Y.encodeStateAsUpdate(providers?.[versionId].document),
|
||||
);
|
||||
|
||||
updateDoc({
|
||||
id: docId,
|
||||
content: newDoc,
|
||||
});
|
||||
updateDoc({
|
||||
id: docId,
|
||||
content: newDoc,
|
||||
});
|
||||
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t('Restore')}
|
||||
</Button>
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t('Restore')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
size={ModalSize.MEDIUM}
|
||||
title={
|
||||
<Box $gap="1rem">
|
||||
<Text $isMaterialIcon $size="36px" $theme="primary">
|
||||
restore
|
||||
</Text>
|
||||
<Text as="h2" $size="h3" $margin="none">
|
||||
{t('Restore this version?')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text $size="h6" $align="flex-start">
|
||||
{t('Warning')}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Box aria-label={t('Modal confirmation to restore the version')}>
|
||||
<Alert canClose={false} type={VariantType.WARNING}>
|
||||
<Box>
|
||||
<Text>
|
||||
{t('Your current document will revert to this version.')}
|
||||
</Text>
|
||||
<Text>{t('If a member is editing, his works can be lost.')}</Text>
|
||||
</Box>
|
||||
</Alert>
|
||||
<Box>
|
||||
<Text>{t('Your current document will revert to this version.')}</Text>
|
||||
<Text>{t('If a member is editing, his works can be lost.')}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
@@ -0,0 +1,162 @@
|
||||
import { Button, Modal, ModalSize, useModal } from '@openfun/cunningham-react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle, css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
|
||||
import { DocEditor } from '../../doc-editor/components/DocEditor';
|
||||
import { Doc } from '../../doc-management';
|
||||
import { Versions } from '../types';
|
||||
|
||||
import { ModalConfirmationVersion } from './ModalConfirmationVersion';
|
||||
import { VersionList } from './VersionList';
|
||||
|
||||
const NoPaddingStyle = createGlobalStyle`
|
||||
.c__modal__scroller:has(.noPadding) {
|
||||
padding: 0 !important;
|
||||
|
||||
.c__modal__close .c__button {
|
||||
right: 0;
|
||||
top: 7px;
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type ModalSelectVersionProps = {
|
||||
doc: Doc;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const ModalSelectVersion = ({
|
||||
onClose,
|
||||
doc,
|
||||
}: ModalSelectVersionProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedVersionId, setSelectedVersionId] =
|
||||
useState<Versions['version_id']>();
|
||||
|
||||
const restoreModal = useModal();
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isOpen
|
||||
hideCloseButton={true}
|
||||
closeOnClickOutside={true}
|
||||
size={ModalSize.EXTRA_LARGE}
|
||||
onClose={onClose}
|
||||
>
|
||||
<NoPaddingStyle />
|
||||
<Box
|
||||
aria-label="version history modal"
|
||||
className="noPadding"
|
||||
$direction="row"
|
||||
$height="100%"
|
||||
$maxHeight="calc(100vh - 2em - 12px)"
|
||||
$overflow="hidden"
|
||||
>
|
||||
<Box
|
||||
$css={css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
$width="100%"
|
||||
$padding={{ horizontal: 'base', vertical: 'xl' }}
|
||||
$align="center"
|
||||
>
|
||||
{selectedVersionId && (
|
||||
<DocEditor doc={doc} versionId={selectedVersionId} />
|
||||
)}
|
||||
{!selectedVersionId && (
|
||||
<Box $align="center" $justify="center" $height="100%">
|
||||
<Text $size="h6" $weight="bold">
|
||||
{t('Select a version on the right to restore')}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
$direction="column"
|
||||
$justify="space-between"
|
||||
$width="250px"
|
||||
$height="100%"
|
||||
$maxHeight="calc(100vh - 2em - 12px)"
|
||||
$css={css`
|
||||
overflow-y: hidden;
|
||||
border-left: 1px solid var(--c--theme--colors--greyscale-200);
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
aria-label="version list"
|
||||
$css={css`
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
$width="100%"
|
||||
$justify="space-between"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$css={css`
|
||||
border-bottom: 1px solid
|
||||
var(--c--theme--colors--greyscale-200);
|
||||
`}
|
||||
$padding="sm"
|
||||
>
|
||||
<Text $size="h6" $variation="1000" $weight="bold">
|
||||
{t('History')}
|
||||
</Text>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
size="nano"
|
||||
color="primary-text"
|
||||
icon={<Icon iconName="close" />}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<VersionList
|
||||
doc={doc}
|
||||
onSelectVersion={setSelectedVersionId}
|
||||
selectedVersionId={selectedVersionId}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
$padding="xs"
|
||||
$css={css`
|
||||
border-top: 1px solid var(--c--theme--colors--greyscale-200);
|
||||
`}
|
||||
>
|
||||
<Button
|
||||
fullWidth
|
||||
disabled={!selectedVersionId}
|
||||
onClick={restoreModal.open}
|
||||
color="primary"
|
||||
>
|
||||
{t('Restore')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
{restoreModal.isOpen && selectedVersionId && (
|
||||
<ModalConfirmationVersion
|
||||
onClose={() => {
|
||||
restoreModal.close();
|
||||
onClose();
|
||||
setSelectedVersionId(undefined);
|
||||
}}
|
||||
docId={doc.id}
|
||||
versionId={selectedVersionId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +1,17 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import React, { PropsWithChildren, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Box, DropButton, IconOptions, StyledLink, Text } from '@/components';
|
||||
import { Box, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
|
||||
import { Versions } from '../types';
|
||||
|
||||
import { ModalVersion } from './ModalVersion';
|
||||
import { ModalConfirmationVersion } from './ModalConfirmationVersion';
|
||||
|
||||
interface VersionItemProps {
|
||||
docId: Doc['id'];
|
||||
text: string;
|
||||
link: string;
|
||||
|
||||
versionId?: Versions['version_id'];
|
||||
isActive: boolean;
|
||||
}
|
||||
@@ -22,81 +20,47 @@ export const VersionItem = ({
|
||||
docId,
|
||||
versionId,
|
||||
text,
|
||||
link,
|
||||
|
||||
isActive,
|
||||
}: VersionItemProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const [isDropOpen, setIsDropOpen] = useState(false);
|
||||
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
|
||||
const spacing = spacingsTokens();
|
||||
|
||||
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
$width="100%"
|
||||
as="li"
|
||||
$background={isActive ? colorsTokens()['primary-300'] : 'transparent'}
|
||||
$background={isActive ? colorsTokens()['greyscale-100'] : 'transparent'}
|
||||
$radius={spacing['3xs']}
|
||||
$css={`
|
||||
border-left: 4px solid transparent;
|
||||
border-bottom: 1px solid ${colorsTokens()['primary-100']};
|
||||
&:hover{
|
||||
border-left: 4px solid ${colorsTokens()['primary-400']};
|
||||
background: ${colorsTokens()['primary-300']};
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: ${colorsTokens()['greyscale-100']};
|
||||
}
|
||||
`}
|
||||
$hasTransition
|
||||
$minWidth="13rem"
|
||||
>
|
||||
<Link href={link} isActive={isActive}>
|
||||
<Box
|
||||
$padding={{ vertical: '0.7rem', horizontal: 'small' }}
|
||||
$align="center"
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$width="100%"
|
||||
>
|
||||
<Box $direction="row" $gap="0.5rem" $align="center">
|
||||
<Text
|
||||
$isMaterialIcon
|
||||
$size="24px"
|
||||
$theme="primary"
|
||||
$variation="600"
|
||||
>
|
||||
description
|
||||
</Text>
|
||||
<Text $weight="bold" $theme="primary" $size="m" $variation="600">
|
||||
{text}
|
||||
</Text>
|
||||
</Box>
|
||||
{isActive && versionId && (
|
||||
<DropButton
|
||||
button={
|
||||
<IconOptions
|
||||
isOpen={isDropOpen}
|
||||
aria-label={t('Open the version options')}
|
||||
/>
|
||||
}
|
||||
onOpenChange={(isOpen) => setIsDropOpen(isOpen)}
|
||||
isOpen={isDropOpen}
|
||||
>
|
||||
<Box>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsModalVersionOpen(true);
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">save</span>}
|
||||
size="small"
|
||||
>
|
||||
{t('Restore the version')}
|
||||
</Button>
|
||||
</Box>
|
||||
</DropButton>
|
||||
)}
|
||||
<Box
|
||||
$padding={{ vertical: '0.7rem', horizontal: 'small' }}
|
||||
$align="center"
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$width="100%"
|
||||
>
|
||||
<Box $direction="row" $gap="0.5rem" $align="center">
|
||||
<Text $weight="bold" $size="sm" $variation="1000">
|
||||
{text}
|
||||
</Text>
|
||||
</Box>
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
{isModalVersionOpen && versionId && (
|
||||
<ModalVersion
|
||||
<ModalConfirmationVersion
|
||||
onClose={() => setIsModalVersionOpen(false)}
|
||||
docId={docId}
|
||||
versionId={versionId}
|
||||
@@ -105,16 +69,3 @@ export const VersionItem = ({
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface LinkProps {
|
||||
href: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const Link = ({ href, children, isActive }: PropsWithChildren<LinkProps>) => {
|
||||
return isActive ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<StyledLink href={href}>{children}</StyledLink>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { APIError } from '@/api';
|
||||
import { Box, InfiniteScroll, Text, TextErrors } from '@/components';
|
||||
import { Box, BoxButton, InfiniteScroll, Text, TextErrors } from '@/components';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import { useDate } from '@/hook';
|
||||
|
||||
@@ -18,19 +18,20 @@ interface VersionListStateProps {
|
||||
error: APIError<unknown> | null;
|
||||
versions?: Versions[];
|
||||
doc: Doc;
|
||||
selectedVersionId?: Versions['version_id'];
|
||||
onSelectVersion?: (versionId: Versions['version_id']) => void;
|
||||
}
|
||||
|
||||
const VersionListState = ({
|
||||
onSelectVersion,
|
||||
selectedVersionId,
|
||||
|
||||
isLoading,
|
||||
error,
|
||||
versions,
|
||||
doc,
|
||||
}: VersionListStateProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { formatDate } = useDate();
|
||||
const {
|
||||
query: { versionId },
|
||||
} = useRouter();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -41,26 +42,23 @@ const VersionListState = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<VersionItem
|
||||
text={t('Current version')}
|
||||
versionId={undefined}
|
||||
link={`/docs/${doc.id}/`}
|
||||
docId={doc.id}
|
||||
isActive={!versionId}
|
||||
/>
|
||||
<Box $gap="10px" $padding="xs">
|
||||
{versions?.map((version) => (
|
||||
<VersionItem
|
||||
<BoxButton
|
||||
aria-label="version item"
|
||||
className="version-item"
|
||||
key={version.version_id}
|
||||
versionId={version.version_id}
|
||||
text={formatDate(version.last_modified, {
|
||||
dateStyle: 'long',
|
||||
timeStyle: 'short',
|
||||
})}
|
||||
link={`/docs/${doc.id}/versions/${version.version_id}`}
|
||||
docId={doc.id}
|
||||
isActive={version.version_id === versionId}
|
||||
/>
|
||||
onClick={() => {
|
||||
onSelectVersion?.(version.version_id);
|
||||
}}
|
||||
>
|
||||
<VersionItem
|
||||
versionId={version.version_id}
|
||||
text={formatDate(version.last_modified, DateTime.DATETIME_MED)}
|
||||
docId={doc.id}
|
||||
isActive={version.version_id === selectedVersionId}
|
||||
/>
|
||||
</BoxButton>
|
||||
))}
|
||||
{error && (
|
||||
<Box
|
||||
@@ -79,15 +77,23 @@ const VersionListState = ({
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface VersionListProps {
|
||||
doc: Doc;
|
||||
onSelectVersion?: (versionId: Versions['version_id']) => void;
|
||||
selectedVersionId?: Versions['version_id'];
|
||||
}
|
||||
|
||||
export const VersionList = ({ doc }: VersionListProps) => {
|
||||
export const VersionList = ({
|
||||
doc,
|
||||
onSelectVersion,
|
||||
selectedVersionId,
|
||||
}: VersionListProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
@@ -98,7 +104,7 @@ export const VersionList = ({ doc }: VersionListProps) => {
|
||||
} = useDocVersionsInfiniteQuery({
|
||||
docId: doc.id,
|
||||
});
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const versions = useMemo(() => {
|
||||
return data?.pages.reduce((acc, page) => {
|
||||
return acc.concat(page.versions);
|
||||
@@ -106,24 +112,32 @@ export const VersionList = ({ doc }: VersionListProps) => {
|
||||
}, [data?.pages]);
|
||||
|
||||
return (
|
||||
<Box $css="overflow-y: auto; overflow-x: hidden;" ref={containerRef}>
|
||||
<Box $css="overflow-y: auto; overflow-x: hidden;">
|
||||
<InfiniteScroll
|
||||
hasMore={hasNextPage}
|
||||
isLoading={isFetchingNextPage}
|
||||
next={() => {
|
||||
void fetchNextPage();
|
||||
}}
|
||||
scrollContainer={containerRef.current}
|
||||
as="ul"
|
||||
$padding="none"
|
||||
$margin={{ top: 'none' }}
|
||||
role="listbox"
|
||||
>
|
||||
{versions?.length === 0 && (
|
||||
<Box $align="center" $margin="large">
|
||||
<Text $size="h6" $weight="bold">
|
||||
{t('No versions')}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<VersionListState
|
||||
onSelectVersion={onSelectVersion}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
versions={versions}
|
||||
doc={doc}
|
||||
selectedVersionId={selectedVersionId}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</Box>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './ModalVersion';
|
||||
export * from './ModalConfirmationVersion';
|
||||
export * from './VersionList';
|
||||
|
||||
@@ -1,220 +1,128 @@
|
||||
import {
|
||||
Column,
|
||||
DataGrid,
|
||||
SortModel,
|
||||
usePagination,
|
||||
} from '@openfun/cunningham-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Loader } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
import { InView } from 'react-intersection-observer';
|
||||
|
||||
import { Card, StyledLink, Text, TextErrors } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import {
|
||||
Doc,
|
||||
DocsOrdering,
|
||||
LinkReach,
|
||||
currentDocRole,
|
||||
isDocsOrdering,
|
||||
useDocs,
|
||||
useTrans,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { useDate } from '@/hook/';
|
||||
import { Box, Card, Text } from '@/components';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { PAGE_SIZE } from '../conf';
|
||||
import { DocDefaultFilter, useInfiniteDocs } from '../../doc-management';
|
||||
|
||||
import { DocsGridActions } from './DocsGridActions';
|
||||
import { DocsGridItem } from './DocsGridItem';
|
||||
import { DocsGridLoader } from './DocsGridLoader';
|
||||
|
||||
const DocsGridStyle = createGlobalStyle`
|
||||
& .c__datagrid thead{
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #fff;
|
||||
z-index: 1;
|
||||
}
|
||||
& .c__pagination__goto{
|
||||
display:none;
|
||||
}
|
||||
`;
|
||||
|
||||
type SortModelItem = {
|
||||
field: string;
|
||||
sort: 'asc' | 'desc' | null;
|
||||
type DocsGridProps = {
|
||||
target?: DocDefaultFilter;
|
||||
};
|
||||
|
||||
function formatSortModel(sortModel: SortModelItem): DocsOrdering | undefined {
|
||||
const { field, sort } = sortModel;
|
||||
const orderingField = sort === 'desc' ? `-${field}` : field;
|
||||
|
||||
if (isDocsOrdering(orderingField)) {
|
||||
return orderingField;
|
||||
}
|
||||
}
|
||||
|
||||
export const DocsGrid = () => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { transRole } = useTrans();
|
||||
export const DocsGrid = ({
|
||||
target = DocDefaultFilter.ALL_DOCS,
|
||||
}: DocsGridProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { formatDate } = useDate();
|
||||
const pagination = usePagination({
|
||||
pageSize: PAGE_SIZE,
|
||||
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
|
||||
const {
|
||||
data,
|
||||
isFetching,
|
||||
isRefetching,
|
||||
isLoading,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
} = useInfiniteDocs({
|
||||
page: 1,
|
||||
...(target &&
|
||||
target !== DocDefaultFilter.ALL_DOCS && {
|
||||
is_creator_me: target === DocDefaultFilter.MY_DOCS,
|
||||
}),
|
||||
});
|
||||
const [sortModel, setSortModel] = useState<SortModel>([
|
||||
{
|
||||
field: 'updated_at',
|
||||
sort: 'desc',
|
||||
},
|
||||
]);
|
||||
const { page, pageSize, setPagesCount } = pagination;
|
||||
const [docs, setDocs] = useState<Doc[]>([]);
|
||||
const { isMobile } = useResponsiveStore();
|
||||
const loading = isFetching || isLoading;
|
||||
|
||||
const ordering = sortModel.length ? formatSortModel(sortModel[0]) : undefined;
|
||||
|
||||
const { data, isLoading, error } = useDocs({
|
||||
page,
|
||||
ordering,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
const loadMore = (inView: boolean) => {
|
||||
if (!inView || loading) {
|
||||
return;
|
||||
}
|
||||
void fetchNextPage();
|
||||
};
|
||||
|
||||
setDocs(data?.results || []);
|
||||
}, [data?.results, t, isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
setPagesCount(data?.count ? Math.ceil(data.count / pageSize) : 0);
|
||||
}, [data?.count, pageSize, setPagesCount]);
|
||||
|
||||
const columns: Column<Doc>[] = [
|
||||
{
|
||||
headerName: '',
|
||||
id: 'visibility',
|
||||
size: 95,
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
row.link_reach === LinkReach.PUBLIC && (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text
|
||||
$weight="bold"
|
||||
$background={colorsTokens()['primary-600']}
|
||||
$color="white"
|
||||
$padding="xtiny"
|
||||
$radius="3px"
|
||||
>
|
||||
{t('Public')}
|
||||
</Text>
|
||||
</StyledLink>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Document name'),
|
||||
field: 'title',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text $weight="bold" $theme="primary">
|
||||
{row.title}
|
||||
</Text>
|
||||
</StyledLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Created at'),
|
||||
field: 'created_at',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text $weight="bold">{formatDate(row.created_at)}</Text>
|
||||
</StyledLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Updated at'),
|
||||
field: 'updated_at',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text $weight="bold">{formatDate(row.updated_at)}</Text>
|
||||
</StyledLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Your role'),
|
||||
id: 'your_role',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text $weight="bold">
|
||||
{transRole(currentDocRole(row.abilities))}
|
||||
</Text>
|
||||
</StyledLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Members'),
|
||||
id: 'users_number',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text $weight="bold">{row.nb_accesses}</Text>
|
||||
</StyledLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'column-actions',
|
||||
renderCell: ({ row }) => {
|
||||
return <DocsGridActions doc={row} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Inverse columns for mobile to have the most important information first
|
||||
if (isMobile) {
|
||||
const tmpCol = columns[0];
|
||||
columns[0] = columns[1];
|
||||
columns[1] = tmpCol;
|
||||
}
|
||||
const title =
|
||||
target === DocDefaultFilter.MY_DOCS
|
||||
? t('My docs')
|
||||
: target === DocDefaultFilter.SHARED_WITH_ME
|
||||
? t('Shared with me')
|
||||
: t('All docs');
|
||||
|
||||
return (
|
||||
<Card
|
||||
$padding={{ bottom: 'small', horizontal: 'big' }}
|
||||
$margin={{ all: isMobile ? 'small' : 'big', top: 'none' }}
|
||||
$overflow="auto"
|
||||
aria-label={t(`Datagrid of the documents page {{page}}`, { page })}
|
||||
$height="100%"
|
||||
>
|
||||
<DocsGridStyle />
|
||||
<Text
|
||||
$weight="bold"
|
||||
as="h2"
|
||||
$theme="primary"
|
||||
$margin={{ bottom: 'small' }}
|
||||
<Box $position="relative" $width="100%" $maxWidth="960px">
|
||||
<DocsGridLoader isLoading={isRefetching} />
|
||||
<Card
|
||||
data-testid="docs-grid"
|
||||
$padding={{ top: 'base', horizontal: 'md', bottom: 'md' }}
|
||||
>
|
||||
{t('Documents')}
|
||||
</Text>
|
||||
<Text
|
||||
as="h4"
|
||||
$size="h4"
|
||||
$variation="1000"
|
||||
$weight="700"
|
||||
$margin={{ top: '0px', bottom: '10px' }}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{error && <TextErrors causes={error.cause} />}
|
||||
<Box $gap="6px">
|
||||
<Box
|
||||
$direction="row"
|
||||
$padding={{ horizontal: 'xs' }}
|
||||
$gap="20px"
|
||||
data-testid="docs-grid-header"
|
||||
>
|
||||
<Box $flex={6} $padding="3xs">
|
||||
<Text $size="xs" $weight="400" $variation="600">
|
||||
{t('Name')}
|
||||
</Text>
|
||||
</Box>
|
||||
{isDesktop && (
|
||||
<Box $flex={1.4} $padding="3xs">
|
||||
<Text $size="xs" $weight="400" $variation="600">
|
||||
{t('Updated at')}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
rows={docs}
|
||||
isLoading={isLoading}
|
||||
pagination={pagination}
|
||||
onSortModelChange={setSortModel}
|
||||
sortModel={sortModel}
|
||||
emptyPlaceholderLabel={t("You don't have any document yet.")}
|
||||
/>
|
||||
</Card>
|
||||
<Box $flex={0.9} $align="flex-end" $padding="3xs" />
|
||||
</Box>
|
||||
|
||||
{/* Body */}
|
||||
{data?.pages.map((currentPage) => {
|
||||
return currentPage.results.map((doc) => (
|
||||
<DocsGridItem doc={doc} key={doc.id} />
|
||||
));
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{loading && (
|
||||
<Box
|
||||
data-testid="docs-grid-loader"
|
||||
$padding="md"
|
||||
$align="center"
|
||||
$justify="center"
|
||||
$width="100%"
|
||||
>
|
||||
<Loader />
|
||||
</Box>
|
||||
)}
|
||||
{hasNextPage && !loading && (
|
||||
<InView
|
||||
data-testid="infinite-scroll-trigger"
|
||||
as="div"
|
||||
onChange={loadMore}
|
||||
>
|
||||
{!isFetching && hasNextPage && (
|
||||
<Button onClick={() => void fetchNextPage()} color="primary-text">
|
||||
{t('More docs')}
|
||||
</Button>
|
||||
)}
|
||||
</InView>
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import React, { useState } from 'react';
|
||||
import { useModal } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { DropdownMenu, DropdownMenuOption, Icon } from '@/components';
|
||||
import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management';
|
||||
|
||||
interface DocsGridActionsProps {
|
||||
@@ -10,26 +10,31 @@ interface DocsGridActionsProps {
|
||||
|
||||
export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
|
||||
const deleteModal = useModal();
|
||||
|
||||
if (!doc.abilities.destroy) {
|
||||
return null;
|
||||
}
|
||||
const options: DropdownMenuOption[] = [
|
||||
{
|
||||
label: t('Remove'),
|
||||
icon: 'delete',
|
||||
callback: () => deleteModal.open(),
|
||||
disabled: !doc.abilities.destroy,
|
||||
testId: `docs-grid-actions-remove-${doc.id}`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsModalRemoveOpen(true);
|
||||
}}
|
||||
color="tertiary-text"
|
||||
icon={<span className="material-icons">delete</span>}
|
||||
size="small"
|
||||
style={{ padding: '0rem' }}
|
||||
aria-label={t('Delete the document')}
|
||||
/>
|
||||
{isModalRemoveOpen && (
|
||||
<ModalRemoveDoc onClose={() => setIsModalRemoveOpen(false)} doc={doc} />
|
||||
<DropdownMenu options={options}>
|
||||
<Icon
|
||||
data-testid={`docs-grid-actions-button-${doc.id}`}
|
||||
iconName="more_horiz"
|
||||
$theme="primary"
|
||||
$variation="600"
|
||||
/>
|
||||
</DropdownMenu>
|
||||
|
||||
{deleteModal.isOpen && (
|
||||
<ModalRemoveDoc onClose={deleteModal.onClose} doc={doc} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { useCreateDoc, useTrans } from '@/features/docs/doc-management/';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocsGrid } from './DocsGrid';
|
||||
|
||||
export const DocsGridContainer = () => {
|
||||
const { t } = useTranslation();
|
||||
const { untitledDocument } = useTrans();
|
||||
const { push } = useRouter();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
|
||||
const { mutate: createDoc } = useCreateDoc({
|
||||
onSuccess: (doc) => {
|
||||
void push(`/docs/${doc.id}`);
|
||||
},
|
||||
});
|
||||
|
||||
const handleCreateDoc = () => {
|
||||
createDoc({ title: untitledDocument });
|
||||
};
|
||||
|
||||
return (
|
||||
<Box $overflow="auto">
|
||||
<Box
|
||||
$align="flex-end"
|
||||
$justify="center"
|
||||
$margin={isMobile ? 'small' : 'big'}
|
||||
>
|
||||
<Button onClick={handleCreateDoc}>{t('Create a new document')}</Button>
|
||||
</Box>
|
||||
<DocsGrid />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, StyledLink, Text } from '@/components';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { Doc, LinkReach } from '../../doc-management';
|
||||
|
||||
import { DocsGridActions } from './DocsGridActions';
|
||||
import { SimpleDocItem } from './SimpleDocItem';
|
||||
|
||||
type DocsGridItemProps = {
|
||||
doc: Doc;
|
||||
};
|
||||
export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
|
||||
const isPublic = doc.link_reach === LinkReach.PUBLIC;
|
||||
const isAuthenticated = doc.link_reach === LinkReach.AUTHENTICATED;
|
||||
const isRestricted = doc.link_reach === LinkReach.RESTRICTED;
|
||||
const sharedCount = doc.nb_accesses - 1;
|
||||
const isShared = sharedCount > 0;
|
||||
|
||||
return (
|
||||
<Box
|
||||
$direction="row"
|
||||
$width="100%"
|
||||
$align="center"
|
||||
$gap="20px"
|
||||
role="row"
|
||||
$padding={{ vertical: '2xs', horizontal: 'sm' }}
|
||||
$css={css`
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
}
|
||||
`}
|
||||
>
|
||||
<StyledLink $css="flex: 7; align-items: center;" href={`/docs/${doc.id}`}>
|
||||
<Box
|
||||
data-testid={`docs-grid-name-${doc.id}`}
|
||||
$flex={6}
|
||||
$padding={{ right: 'base' }}
|
||||
>
|
||||
<SimpleDocItem doc={doc} />
|
||||
</Box>
|
||||
{isDesktop && (
|
||||
<Box $flex={1.3}>
|
||||
<Text $variation="500" $size="xs">
|
||||
{DateTime.fromISO(doc.updated_at).toRelative()}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</StyledLink>
|
||||
<Box
|
||||
$flex={0.9}
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$justify="flex-end"
|
||||
$gap="10px"
|
||||
>
|
||||
{isDesktop && isPublic && (
|
||||
<Button
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
size="nano"
|
||||
fullWidth
|
||||
icon={<Icon $variation="000" iconName="public" />}
|
||||
>
|
||||
{isShared ? sharedCount : undefined}
|
||||
</Button>
|
||||
)}
|
||||
{isDesktop && !isPublic && isRestricted && isShared && (
|
||||
<Button
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
fullWidth
|
||||
color="tertiary"
|
||||
size="nano"
|
||||
icon={<Icon $variation="800" $theme="primary" iconName="group" />}
|
||||
>
|
||||
{sharedCount}
|
||||
</Button>
|
||||
)}
|
||||
{isDesktop && !isPublic && isAuthenticated && (
|
||||
<Button
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
size="nano"
|
||||
fullWidth
|
||||
icon={<Icon $variation="000" iconName="corporate_fare" />}
|
||||
>
|
||||
{sharedCount}
|
||||
</Button>
|
||||
)}
|
||||
<DocsGridActions doc={doc} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { createGlobalStyle, css } from 'styled-components';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { HEADER_HEIGHT } from '@/features/header/conf';
|
||||
|
||||
const DocsGridLoaderStyle = createGlobalStyle`
|
||||
body, main {
|
||||
overflow: hidden!important;
|
||||
overflow-y: hidden!important;
|
||||
}
|
||||
`;
|
||||
|
||||
type DocsGridLoaderProps = {
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
export const DocsGridLoader = ({ isLoading }: DocsGridLoaderProps) => {
|
||||
if (!isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocsGridLoaderStyle />
|
||||
<Box
|
||||
data-testid="grid-loader"
|
||||
$align="center"
|
||||
$justify="center"
|
||||
$height="calc(100vh - 50px)"
|
||||
$width="100%"
|
||||
$maxWidth="960px"
|
||||
$background="rgba(255, 255, 255, 0.3)"
|
||||
$zIndex={998}
|
||||
$position="fixed"
|
||||
$css={css`
|
||||
top: ${HEADER_HEIGHT}px;
|
||||
`}
|
||||
>
|
||||
<Loader />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, LinkReach } from '@/features/docs';
|
||||
import PinnedDocumentIcon from '@/features/docs/doc-management/assets/pinned-document.svg';
|
||||
import SimpleFileIcon from '@/features/docs/doc-management/assets/simple-document.svg';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
const ItemTextCss = css`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: initial;
|
||||
display: -webkit-box;
|
||||
line-clamp: 1;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
`;
|
||||
|
||||
type SimpleDocItemProps = {
|
||||
doc: Doc;
|
||||
isPinned?: boolean;
|
||||
subText?: string;
|
||||
};
|
||||
|
||||
export const SimpleDocItem = ({
|
||||
doc,
|
||||
isPinned = false,
|
||||
subText,
|
||||
}: SimpleDocItemProps) => {
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const spacings = spacingsTokens();
|
||||
|
||||
const isPublic = doc?.link_reach === LinkReach.PUBLIC;
|
||||
const isShared = !isPublic && doc.nb_accesses > 1;
|
||||
const accessCount = doc.nb_accesses - 1;
|
||||
const isSharedOrPublic = isShared || isPublic;
|
||||
|
||||
return (
|
||||
<Box $direction="row" $gap={spacings.sm}>
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$css={css`
|
||||
background-color: transparent;
|
||||
filter: drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.05));
|
||||
`}
|
||||
>
|
||||
{isPinned ? <PinnedDocumentIcon /> : <SimpleFileIcon />}
|
||||
</Box>
|
||||
<Box>
|
||||
<Text
|
||||
aria-describedby="doc-title"
|
||||
aria-label={doc.title}
|
||||
$size="sm"
|
||||
$variation="1000"
|
||||
$weight="500"
|
||||
$css={ItemTextCss}
|
||||
>
|
||||
{doc.title}
|
||||
</Text>
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap={spacings['3xs']}
|
||||
$margin={{ top: '-2px' }}
|
||||
>
|
||||
{!isDesktop && (
|
||||
<>
|
||||
{isPublic && <Icon iconName="public" $size="16px" />}
|
||||
{isShared && <Icon iconName="group" $size="16px" />}
|
||||
{isSharedOrPublic && accessCount > 0 && (
|
||||
<Text $size="12px">{accessCount}</Text>
|
||||
)}
|
||||
{isSharedOrPublic && <Text $size="12px">·</Text>}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Text $size="xs" $variation="500" $weight="400" $css={ItemTextCss}>
|
||||
{subText ??
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi vel ante libero. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed imperdiet neque quam, sed euismod metus mollis ut. '}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './DocsGridContainer';
|
||||
@@ -1,31 +1,4 @@
|
||||
<svg viewBox="0 0 36 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5_830)">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M26.5706 16.681V20.756H9.59985V16.681H26.5706ZM26.5706 23.0467V27.1215H9.59985V23.0467H26.5706ZM19.5375 29.2926V33.3674H9.59985V29.2926H19.5375Z"
|
||||
fill="#E1000F"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M35.6982 16.7351V33.2911C35.6826 33.2845 35.667 33.2756 35.6492 33.269V37.4663C35.6492 39.8874 33.641 41.8683 31.1867 41.8683H4.461C2.00664 41.8683 -0.00146484 39.8874 -0.00146484 37.4663V4.45137C-0.00146484 2.03028 2.00664 0.0493774 4.461 0.0493774H18.723V4.45137H4.461V37.4663H31.1867V16.7351H35.6982Z"
|
||||
fill="#000091"
|
||||
/>
|
||||
<path
|
||||
d="M33.9524 0.0360107H23.626C22.6794 0.0360107 21.9049 0.799997 21.9049 1.73376V11.9202C21.9049 12.854 22.6794 13.618 23.626 13.618H33.9524C34.899 13.618 35.6735 12.854 35.6735 11.9202V1.73376C35.6735 0.799997 34.899 0.0360107 33.9524 0.0360107ZM26.6378 6.40257C26.6378 7.10713 26.0613 7.67588 25.347 7.67588H24.4865V8.73697C24.4865 9.08501 24.1939 9.37362 23.8411 9.37362C23.4883 9.37362 23.1957 9.08501 23.1957 8.73697V5.12925C23.1957 4.66237 23.5829 4.28038 24.0562 4.28038H25.347C26.0613 4.28038 26.6378 4.84913 26.6378 5.55369V6.40257ZM30.9405 8.10031C30.9405 8.80488 30.364 9.37362 29.6497 9.37362H27.9286C27.6877 9.37362 27.4984 9.18687 27.4984 8.94919V4.70482C27.4984 4.46713 27.6877 4.28038 27.9286 4.28038H29.6497C30.364 4.28038 30.9405 4.84913 30.9405 5.55369V8.10031ZM34.3827 4.91704C34.3827 5.26507 34.0901 5.55369 33.7373 5.55369H33.0919V6.40257H33.7373C34.0901 6.40257 34.3827 6.69118 34.3827 7.03922C34.3827 7.38726 34.0901 7.67588 33.7373 7.67588H33.0919V8.73697C33.0919 9.08501 32.7993 9.37362 32.4465 9.37362C32.0936 9.37362 31.8011 9.08501 31.8011 8.73697V5.12925C31.8011 4.66237 32.1883 4.28038 32.6616 4.28038H33.7373C34.0901 4.28038 34.3827 4.569 34.3827 4.91704ZM24.4865 6.40257H25.347V5.55369H24.4865V6.40257ZM28.7892 8.10031H29.6497V5.55369H28.7892V8.10031Z"
|
||||
fill="#000091"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M35.6734 10.0666V13.6574H32.4942V10.0666H35.6734ZM25.0866 0.0441895V3.59171H21.9041V0.0441895H25.0866Z"
|
||||
fill="#000091"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5_830">
|
||||
<rect width="36" height="42" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.6305 28.8312C22.7983 28.5038 23.9166 27.9062 24.6505 26.8503C25.3749 25.8163 25.5789 24.5047 25.5789 23.2425V4.75099C25.5789 4.42358 25.5611 4.09557 25.5216 3.77148C26.1016 3.99961 26.5486 4.37658 26.8626 4.90239C27.2331 5.50024 27.4184 6.28757 27.4184 7.26435V26.0464C27.4184 27.3684 27.0942 28.3578 26.4458 29.0146C25.7974 29.6714 24.8207 29.9998 23.5155 29.9998H16.4209C16.5889 29.9704 16.7574 29.9401 16.9262 29.909C18.4067 29.6444 19.9713 29.2854 21.6185 28.8346L21.6305 28.8312Z" fill="#C9191E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.58203 25.655V6.8477C4.58203 5.70251 4.88938 4.83519 5.50408 4.24575C6.1272 3.65631 6.95242 3.33212 7.97972 3.27318C9.49542 3.18055 10.9311 3.05425 12.2868 2.89425C13.6425 2.72584 14.9393 2.53217 16.1771 2.31324C17.4234 2.0943 18.6359 1.85011 19.8148 1.58065C21.0274 1.29435 21.9578 1.4375 22.6062 2.0101C23.2546 2.58269 23.5788 3.49632 23.5788 4.75099V23.2425C23.5788 24.3456 23.3893 25.1666 23.0104 25.7055C22.6315 26.2529 21.9915 26.6528 21.0905 26.9054C19.4906 27.3433 17.9833 27.6886 16.5687 27.9412C15.154 28.2022 13.7731 28.4001 12.4258 28.5348C11.0785 28.6696 9.69751 28.7748 8.28286 28.8506C7.11241 28.918 6.20299 28.6738 5.5546 28.118C4.90622 27.5707 4.58203 26.7497 4.58203 25.655ZM9.20865 10.2624C11.0635 10.1444 12.7632 9.96305 14.3075 9.71831C14.6822 9.65722 15.0564 9.5936 15.4291 9.52759C15.8192 9.45851 16.1013 9.11859 16.1013 8.72337C16.1013 8.21154 15.638 7.82609 15.135 7.91189C14.846 7.96118 14.5555 8.00909 14.2635 8.05562C12.7346 8.29923 11.0452 8.47998 9.19523 8.5977C8.91819 8.61558 8.69776 8.70188 8.55608 8.87391C8.42209 9.03661 8.35645 9.23229 8.35645 9.45535C8.35645 9.68212 8.43296 9.87951 8.58568 10.0418L8.58783 10.0439C8.75336 10.2095 8.96369 10.2811 9.20865 10.2624ZM9.20801 14.456C11.0631 14.338 12.763 14.1566 14.3075 13.9119C15.8588 13.6589 17.3936 13.3638 18.9112 13.0266C19.2191 12.9581 19.4498 12.8503 19.5652 12.683C19.6786 12.5221 19.7347 12.3376 19.7347 12.1332C19.7347 11.9026 19.6469 11.704 19.476 11.5426C19.2921 11.3689 19.0348 11.3284 18.7304 11.3911L18.7285 11.3915C17.2823 11.7194 15.794 12.0053 14.2635 12.2492C12.7346 12.4928 11.0452 12.6735 9.19523 12.7913C8.91819 12.8091 8.69776 12.8954 8.55608 13.0675C8.42276 13.2294 8.35645 13.4205 8.35645 13.6363C8.35645 13.8703 8.43209 14.0723 8.58558 14.2354L8.59 14.2396C8.75499 14.3949 8.96316 14.4655 9.20551 14.4562L9.20801 14.456ZM9.20847 18.6494C11.0634 18.5229 12.7631 18.3374 14.3075 18.0927C15.8589 17.8482 17.3934 17.5573 18.9112 17.22C19.2199 17.1514 19.4508 17.0391 19.566 16.8627C19.6783 16.7029 19.7347 16.5233 19.7347 16.3266C19.7347 16.0961 19.6469 15.8974 19.476 15.7361C19.2921 15.5623 19.0348 15.5218 18.7304 15.5845L18.729 15.5848C17.2827 15.9043 15.7942 16.1861 14.2635 16.43C12.7345 16.6736 11.045 16.8586 9.19495 16.9847C8.91804 17.0026 8.69771 17.0889 8.55608 17.2609C8.42276 17.4228 8.35645 17.6139 8.35645 17.8297C8.35645 18.0637 8.43209 18.2658 8.58558 18.4289L8.59 18.433C8.75499 18.5883 8.96316 18.6589 9.20551 18.6496L9.20847 18.6494ZM14.3075 22.257C12.7632 22.5018 11.0635 22.6831 9.20867 22.8012C8.9637 22.8198 8.75337 22.7482 8.58783 22.5826L8.58572 22.5805C8.433 22.4182 8.35645 22.2208 8.35645 21.9941C8.35645 21.771 8.42209 21.5753 8.55608 21.4126C8.69776 21.2406 8.91827 21.1543 9.19531 21.1364C11.0453 21.0187 12.7346 20.838 14.2635 20.5943C14.5555 20.5478 14.846 20.4999 15.135 20.4506C15.638 20.3648 16.1013 20.7503 16.1013 21.2621C16.1013 21.6573 15.8192 21.9972 15.4291 22.0663C15.0564 22.1323 14.6822 22.1959 14.3075 22.257Z" fill="#000091"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -1,32 +0,0 @@
|
||||
.burgerIcon {
|
||||
cursor: pointer;
|
||||
transform: translate(-20%, 0%);
|
||||
}
|
||||
|
||||
.burgerIcon path {
|
||||
stroke-width: 40;
|
||||
stroke-linecap: round;
|
||||
fill: none;
|
||||
transition: all 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* In menu form */
|
||||
.burgerIcon path:first-child,
|
||||
.burgerIcon path:last-child {
|
||||
stroke-dasharray: 240px 910px;
|
||||
}
|
||||
|
||||
.burgerIcon .middle_bar {
|
||||
stroke-dasharray: 240px 240px;
|
||||
}
|
||||
|
||||
/* In cross form */
|
||||
.open path:first-child,
|
||||
.open path:last-child {
|
||||
stroke-dashoffset: -650px;
|
||||
}
|
||||
|
||||
.open :nth-child(2) {
|
||||
stroke-dasharray: 0 220px;
|
||||
stroke-dashoffset: -120px;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Burger } from './Burger';
|
||||
|
||||
describe('<Burger />', () => {
|
||||
test('Burger interactions', () => {
|
||||
const { rerender } = render(<Burger isOpen={true} />);
|
||||
|
||||
const burger = screen.getByRole('img');
|
||||
expect(burger).toBeInTheDocument();
|
||||
expect(burger.classList.contains('open')).toBeTruthy();
|
||||
|
||||
rerender(<Burger isOpen={false} />);
|
||||
|
||||
expect(burger.classList.contains('open')).not.toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import { SVGProps } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import styles from './Burger.module.css';
|
||||
import BurgerIcon from './burger.svg';
|
||||
|
||||
type BurgerProps = SVGProps<SVGSVGElement> & {
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
export const Burger = ({ isOpen, ...props }: BurgerProps) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
$color={colorsTokens()['primary-text']}
|
||||
$padding="none"
|
||||
$justify="center"
|
||||
>
|
||||
<BurgerIcon
|
||||
role="img"
|
||||
className={`${styles.burgerIcon} ${isOpen ? styles.open : ''}`}
|
||||
{...props}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Burger;
|
||||
@@ -1,10 +0,0 @@
|
||||
<svg viewBox="280 230 400 200" stroke="currentColor">
|
||||
<path
|
||||
d="M300,220 C300,220 520,220 540,220 C740,220 640,540 520,420 C440,340 300,200 300,200"
|
||||
/>
|
||||
<path d="M300,320 L540,320" />
|
||||
<path
|
||||
d="M300,210 C300,210 520,210 540,210 C740,210 640,530 520,410 C440,330 300,190 300,190"
|
||||
transform="translate(480, 320) scale(1, -1) translate(-480, -318)"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 375 B |
@@ -1,46 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, DropButton } from '@/components';
|
||||
import { ButtonLogin } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { LanguagePicker } from '@/features/language';
|
||||
|
||||
import { Burger } from './Burger/Burger';
|
||||
|
||||
export const DropdownMenu = () => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const [isDropOpen, setIsDropOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<DropButton
|
||||
button={
|
||||
<Burger
|
||||
isOpen={isDropOpen}
|
||||
width={30}
|
||||
height={30}
|
||||
aria-controls="menu"
|
||||
aria-label={t('Open the header menu')}
|
||||
/>
|
||||
}
|
||||
onOpenChange={(isOpen) => setIsDropOpen(isOpen)}
|
||||
isOpen={isDropOpen}
|
||||
>
|
||||
<Box $align="center" $direction="column">
|
||||
<Box
|
||||
$width="100%"
|
||||
$align="center"
|
||||
$height="36px"
|
||||
$justify="center"
|
||||
$css={`&:hover{background:${colorsTokens()['primary-150']}}`}
|
||||
$hasTransition
|
||||
$radius="2px"
|
||||
>
|
||||
<LanguagePicker />
|
||||
</Box>
|
||||
<ButtonLogin />
|
||||
</Box>
|
||||
</DropButton>
|
||||
);
|
||||
};
|
||||
@@ -1,90 +1,105 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import Image from 'next/image';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, StyledLink, Text } from '@/components/';
|
||||
import { Box, Icon, StyledLink, Text } from '@/components/';
|
||||
import { ButtonLogin } from '@/core/auth';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { LanguagePicker } from '@/features/language';
|
||||
import { useLeftPanelStore } from '@/features/left-panel';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { default as IconDocs } from '../assets/icon-docs.svg?url';
|
||||
import { HEADER_HEIGHT } from '../conf';
|
||||
|
||||
import { DropdownMenu } from './DropdownMenu';
|
||||
import { LaGaufre } from './LaGaufre';
|
||||
|
||||
export const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isSmallMobile } = useResponsiveStore();
|
||||
const theme = useCunninghamTheme();
|
||||
const { isPanelOpen, togglePanel } = useLeftPanelStore();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
|
||||
const spacings = theme.spacingsTokens();
|
||||
const colors = theme.colorsTokens();
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="header"
|
||||
$justify="center"
|
||||
$width="100%"
|
||||
$zIndex="100"
|
||||
$padding={{ vertical: 'xtiny' }}
|
||||
$css="box-shadow: 0 1px 4px #00000040;"
|
||||
$css={css`
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: ${HEADER_HEIGHT}px;
|
||||
min-height: ${HEADER_HEIGHT}px;
|
||||
padding: 0 ${spacings['base']};
|
||||
background-color: ${colors['greyscale-000']};
|
||||
border-bottom: 1px solid ${colors['greyscale-200']};
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
$margin={{
|
||||
left: 'big',
|
||||
right: isSmallMobile ? 'none' : 'big',
|
||||
}}
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
$direction="row"
|
||||
>
|
||||
<Box>
|
||||
<StyledLink href="/">
|
||||
<Box
|
||||
$align="center"
|
||||
$gap="0.8rem"
|
||||
$direction="row"
|
||||
$position="relative"
|
||||
$height="fit-content"
|
||||
$margin={{ top: 'auto' }}
|
||||
{!isDesktop && (
|
||||
<Button
|
||||
size="medium"
|
||||
onClick={togglePanel}
|
||||
aria-label={t('Open the header menu')}
|
||||
color="primary-text"
|
||||
icon={<Icon iconName={isPanelOpen ? 'close' : 'menu'} />}
|
||||
/>
|
||||
)}
|
||||
|
||||
<StyledLink href="/">
|
||||
<Box
|
||||
$align="center"
|
||||
$gap={spacings['3xs']}
|
||||
$direction="row"
|
||||
$position="relative"
|
||||
$height="fit-content"
|
||||
$margin={{ top: 'auto' }}
|
||||
>
|
||||
<Image priority src={IconDocs} alt={t('Docs Logo')} width={25} />
|
||||
|
||||
<Box $direction="row" $align="center" $gap={spacings['2xs']}>
|
||||
<Text
|
||||
$margin="none"
|
||||
as="h2"
|
||||
$color="#000091"
|
||||
$zIndex={1}
|
||||
$size="1.30rem"
|
||||
>
|
||||
<Image priority src={IconDocs} alt={t('Docs Logo')} width={25} />
|
||||
<Text
|
||||
$padding="2px 3px"
|
||||
$size="8px"
|
||||
$background="#368bd6"
|
||||
$color="white"
|
||||
$position="absolute"
|
||||
$radius="5px"
|
||||
$css={`
|
||||
bottom: 13px;
|
||||
right: -17px;
|
||||
`}
|
||||
>
|
||||
BETA
|
||||
</Text>
|
||||
<Text
|
||||
$margin="none"
|
||||
as="h2"
|
||||
$color="#000091"
|
||||
$zIndex={1}
|
||||
$size="1.30rem"
|
||||
$css="font-family: 'Marianne'"
|
||||
>
|
||||
{t('Docs')}
|
||||
</Text>
|
||||
</Box>
|
||||
</StyledLink>
|
||||
{t('Docs')}
|
||||
</Text>
|
||||
<Text
|
||||
$padding={{ horizontal: 'xs', vertical: '1px' }}
|
||||
$size="11px"
|
||||
$theme="primary"
|
||||
$variation="500"
|
||||
$weight="bold"
|
||||
$radius="12px"
|
||||
$background={colors['primary-200']}
|
||||
>
|
||||
BETA
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{isSmallMobile ? (
|
||||
<Box $direction="row" $gap="2rem">
|
||||
<LaGaufre />
|
||||
<DropdownMenu />
|
||||
</Box>
|
||||
) : (
|
||||
<Box $align="center" $gap="2vw" $direction="row">
|
||||
<ButtonLogin />
|
||||
<LanguagePicker />
|
||||
<LaGaufre />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</StyledLink>
|
||||
{!isDesktop ? (
|
||||
<Box $direction="row" $gap={spacings['sm']}>
|
||||
<LaGaufre />
|
||||
</Box>
|
||||
) : (
|
||||
<Box $align="center" $gap={spacings['sm']} $direction="row">
|
||||
<ButtonLogin />
|
||||
<LanguagePicker />
|
||||
<LaGaufre />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
1
src/frontend/apps/impress/src/features/header/conf.ts
Normal file
1
src/frontend/apps/impress/src/features/header/conf.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const HEADER_HEIGHT = 52;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Select } from '@openfun/cunningham-react';
|
||||
import { Settings } from 'luxon';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styled from 'styled-components';
|
||||
@@ -13,6 +14,7 @@ const SelectStyled = styled(Select)<{ $isSmall?: boolean }>`
|
||||
.c__select__wrapper {
|
||||
min-height: 2rem;
|
||||
height: auto;
|
||||
|
||||
border-color: transparent;
|
||||
padding: 0 0.15rem 0 0.45rem;
|
||||
border-radius: 1px;
|
||||
@@ -26,7 +28,7 @@ const SelectStyled = styled(Select)<{ $isSmall?: boolean }>`
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--c--theme--colors--primary-100) 0 0 0 2px !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -34,6 +36,7 @@ const SelectStyled = styled(Select)<{ $isSmall?: boolean }>`
|
||||
export const LanguagePicker = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { preload: languages } = i18n.options;
|
||||
Settings.defaultLocale = i18n.language;
|
||||
|
||||
const optionsPicker = useMemo(() => {
|
||||
return (languages || []).map((lang) => ({
|
||||
@@ -46,10 +49,16 @@ export const LanguagePicker = () => {
|
||||
$gap="0.7rem"
|
||||
$align="center"
|
||||
>
|
||||
<Text $isMaterialIcon $size="1rem" $theme="primary" $variation="600">
|
||||
<Text
|
||||
$isMaterialIcon
|
||||
$size="1rem"
|
||||
$theme="primary"
|
||||
$weight="bold"
|
||||
$variation="800"
|
||||
>
|
||||
translate
|
||||
</Text>
|
||||
<Text $theme="primary" $variation="600">
|
||||
<Text $theme="primary" $weight="500" $variation="800">
|
||||
{LANGUAGES_ALLOWED[lang]}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, Icon, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { DocDefaultFilter } from '@/features/docs';
|
||||
import { useLeftPanelStore } from '@/features/left-panel';
|
||||
|
||||
export const LeftPanelTargetFilters = () => {
|
||||
const { t } = useTranslation();
|
||||
const pathname = usePathname();
|
||||
const { togglePanel } = useLeftPanelStore();
|
||||
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
|
||||
const spacing = spacingsTokens();
|
||||
const colors = colorsTokens();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const target =
|
||||
(searchParams.get('target') as DocDefaultFilter) ??
|
||||
DocDefaultFilter.ALL_DOCS;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const defaultQueries = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
icon: 'apps',
|
||||
label: t('All docs'),
|
||||
targetQuery: DocDefaultFilter.ALL_DOCS,
|
||||
},
|
||||
{
|
||||
icon: 'lock',
|
||||
label: t('My docs'),
|
||||
targetQuery: DocDefaultFilter.MY_DOCS,
|
||||
},
|
||||
{
|
||||
icon: 'group',
|
||||
label: t('Shared with me'),
|
||||
targetQuery: DocDefaultFilter.SHARED_WITH_ME,
|
||||
},
|
||||
];
|
||||
}, [t]);
|
||||
|
||||
const onSelectQuery = (query: DocDefaultFilter) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set('target', query);
|
||||
router.replace(`${pathname}?${params.toString()}`);
|
||||
togglePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
$justify="center"
|
||||
$padding={{ horizontal: 'xs' }}
|
||||
$gap={spacing['4xs']}
|
||||
>
|
||||
{defaultQueries.map((query) => {
|
||||
const isActive = target === query.targetQuery;
|
||||
|
||||
return (
|
||||
<BoxButton
|
||||
aria-label={query.label}
|
||||
key={query.label}
|
||||
onClick={() => onSelectQuery(query.targetQuery)}
|
||||
$direction="row"
|
||||
aria-selected={isActive}
|
||||
$align="center"
|
||||
$justify="flex-start"
|
||||
$gap={spacing['xs']}
|
||||
$radius={spacing['3xs']}
|
||||
$padding={{ all: '2xs' }}
|
||||
$css={css`
|
||||
cursor: pointer;
|
||||
background-color: ${isActive
|
||||
? colors['greyscale-100']
|
||||
: undefined};
|
||||
font-weight: ${isActive ? 700 : 500};
|
||||
&:hover {
|
||||
background-color: ${colors['greyscale-100']};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
$variation={isActive ? '1000' : '700'}
|
||||
iconName={query.icon}
|
||||
/>
|
||||
<Text $variation={isActive ? '1000' : '700'} $size="sm">
|
||||
{query.label}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import { createGlobalStyle, css } from 'styled-components';
|
||||
|
||||
import { Box, SeparatedSection } from '@/components';
|
||||
import { ButtonLogin } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { HEADER_HEIGHT } from '@/features/header/conf';
|
||||
import { LanguagePicker } from '@/features/language';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useLeftPanelStore } from '../stores';
|
||||
|
||||
import { LeftPanelContent } from './LeftPanelContent';
|
||||
import { LeftPanelHeader } from './LeftPanelHeader';
|
||||
|
||||
const MobileLeftPanelStyle = createGlobalStyle`
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
export const LeftPanel = () => {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { isPanelOpen } = useLeftPanelStore();
|
||||
const theme = useCunninghamTheme();
|
||||
const colors = theme.colorsTokens();
|
||||
const spacings = theme.spacingsTokens();
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDesktop && (
|
||||
<Box
|
||||
data-testid="left-panel-desktop"
|
||||
$css={`
|
||||
height: calc(100vh - ${HEADER_HEIGHT}px);
|
||||
width: 300px;
|
||||
min-width: 300px;
|
||||
border-right: 1px solid ${colors['greyscale-200']};
|
||||
`}
|
||||
>
|
||||
<LeftPanelHeader />
|
||||
<LeftPanelContent />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!isDesktop && (
|
||||
<>
|
||||
{isPanelOpen && <MobileLeftPanelStyle />}
|
||||
<Box
|
||||
$hasTransition
|
||||
$css={css`
|
||||
z-index: 999;
|
||||
width: 100dvw;
|
||||
height: calc(100dvh - 52px);
|
||||
border-right: 1px solid var(--c--theme--colors--greyscale-200);
|
||||
position: fixed;
|
||||
transform: translateX(${isPanelOpen ? '0' : '-100dvw'});
|
||||
background-color: var(--c--theme--colors--greyscale-000);
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
data-testid="left-panel-mobile"
|
||||
$css={css`
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: ${spacings['base']};
|
||||
`}
|
||||
>
|
||||
<LeftPanelHeader />
|
||||
<LeftPanelContent />
|
||||
<SeparatedSection showSeparator={false}>
|
||||
<Box $justify="center" $align="center" $gap={spacings['sm']}>
|
||||
<ButtonLogin />
|
||||
<LanguagePicker />
|
||||
</Box>
|
||||
</SeparatedSection>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { Box, SeparatedSection } from '@/components';
|
||||
|
||||
import { LeftPanelTargetFilters } from './LefPanelTargetFilters';
|
||||
|
||||
export const LeftPanelContent = () => {
|
||||
const router = useRouter();
|
||||
const isHome = router.pathname === '/';
|
||||
|
||||
return (
|
||||
<Box $width="100%">
|
||||
{isHome && (
|
||||
<SeparatedSection>
|
||||
<LeftPanelTargetFilters />
|
||||
</SeparatedSection>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { t } from 'i18next';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { Box, Icon, SeparatedSection } from '@/components';
|
||||
import { useCreateDoc } from '@/features/docs';
|
||||
|
||||
import { useLeftPanelStore } from '../stores';
|
||||
|
||||
export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
|
||||
const router = useRouter();
|
||||
const { togglePanel } = useLeftPanelStore();
|
||||
|
||||
const { mutate: createDoc } = useCreateDoc({
|
||||
onSuccess: (doc) => {
|
||||
router.push(`/docs/${doc.id}`);
|
||||
togglePanel();
|
||||
},
|
||||
});
|
||||
|
||||
const goToHome = () => {
|
||||
router.push('/');
|
||||
togglePanel();
|
||||
};
|
||||
|
||||
const createNewDoc = () => {
|
||||
createDoc({ title: t('Untitled document') });
|
||||
};
|
||||
|
||||
return (
|
||||
<Box $width="100%">
|
||||
<SeparatedSection>
|
||||
<Box
|
||||
$padding={{ horizontal: 'sm' }}
|
||||
$width="100%"
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$align="center"
|
||||
>
|
||||
<Box $direction="row" $gap="2px">
|
||||
<Button
|
||||
onClick={goToHome}
|
||||
size="medium"
|
||||
color="tertiary-text"
|
||||
icon={<Icon iconName="house" />}
|
||||
/>
|
||||
</Box>
|
||||
<Button onClick={createNewDoc}>{t('New doc')}</Button>
|
||||
</Box>
|
||||
</SeparatedSection>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './LeftPanel';
|
||||
@@ -1 +1,2 @@
|
||||
export * from './components';
|
||||
export * from './stores';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './useLeftPanelStore';
|
||||
@@ -0,0 +1,11 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface LeftPanelState {
|
||||
isPanelOpen: boolean;
|
||||
togglePanel: () => void;
|
||||
}
|
||||
|
||||
export const useLeftPanelStore = create<LeftPanelState>((set) => ({
|
||||
isPanelOpen: false,
|
||||
togglePanel: () => set((state) => ({ isPanelOpen: !state.isPanelOpen })),
|
||||
}));
|
||||
@@ -91,7 +91,6 @@ self.addEventListener('activate', function (event) {
|
||||
const FALLBACK = {
|
||||
offline: '/offline/',
|
||||
docs: '/docs/[id]/',
|
||||
versions: '/docs/[id]/versions/[versionId]/',
|
||||
images: '/assets/img-not-found.svg',
|
||||
};
|
||||
const precacheResources = [
|
||||
@@ -104,7 +103,6 @@ const precacheResources = [
|
||||
FALLBACK.offline,
|
||||
FALLBACK.images,
|
||||
FALLBACK.docs,
|
||||
FALLBACK.versions,
|
||||
];
|
||||
|
||||
const precacheStrategy = getStrategy({
|
||||
@@ -125,12 +123,6 @@ setCatchHandler(async ({ request, url, event }) => {
|
||||
case request.destination === 'document':
|
||||
if (url.pathname.match(/^\/docs\/([a-z0-9\-]+)\/$/g)) {
|
||||
return precacheStrategy.handle({ event, request: FALLBACK.docs });
|
||||
} else if (
|
||||
url.pathname.match(
|
||||
/^\/docs\/([a-z0-9\-]+)\/versions\/([a-z0-9\-]+)\/$/g,
|
||||
)
|
||||
) {
|
||||
return precacheStrategy.handle({ event, request: FALLBACK.versions });
|
||||
}
|
||||
|
||||
return precacheStrategy.handle({ event, request: FALLBACK.offline });
|
||||
|
||||
@@ -9,42 +9,29 @@
|
||||
"Anyone on the internet with the link can view": "Für jeden im Internet mit diesem Link sichtbar",
|
||||
"Are you sure you want to delete the document \"{{title}}\"?": "Sind Sie sicher, dass Sie das Dokument \"{{title}}\" löschen möchten?",
|
||||
"Back to home page": "Zurück zur Startseite",
|
||||
"Back to top": "Zurück nach oben",
|
||||
"Can't load this page, please check your internet connection.": "Diese Seite kann nicht geladen werden. Bitte überprüfen Sie Ihre Internetverbindung.",
|
||||
"Cancel": "Abbrechen",
|
||||
"Choose a role": "Wählen Sie eine Rolle",
|
||||
"Close the modal": "Pop up schliessen",
|
||||
"Close the panel": "Fenster schließen",
|
||||
"Compliance status": "Konformitätsstatus",
|
||||
"Confirm deletion": "Löschung bestätigen",
|
||||
"Content modal to delete document": "Inhalts-Modal zum Löschen des Dokuments",
|
||||
"Content modal to export the document": "Inhalte zum Exportieren des Dokuments",
|
||||
"Copy link": "Link kopieren",
|
||||
"Create a new document": "Neues Dokument erstellen",
|
||||
"Created at": "Erstellt am",
|
||||
"Current version": "Aktuelle Version",
|
||||
"Delete document": "Dokument löschen",
|
||||
"Delete the document": "Dokument löschen",
|
||||
"Deleting the document \"{{title}}\"": "Lösche das Dokument \"{{title}}\"",
|
||||
"Doc visibility card": "Dokumenten-Sichtbarkeitskarte",
|
||||
"Docs": "Docs",
|
||||
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Pages: Ihr neuer Begleiter für eine effiziente, intuitive und sichere Zusammenarbeit bei Dokumenten.",
|
||||
"Document icon": "Dokumentensymbol",
|
||||
"Document name": "Dokumentenname",
|
||||
"Document panel": "Dokumenten-Panel",
|
||||
"Document title updated successfully": "Titel des Dokuments erfolgreich aktualisiert",
|
||||
"Documents": "Dokumente",
|
||||
"Docx": "Docx",
|
||||
"Download": "Herunterladen",
|
||||
"E-mail:": "E-Mail:",
|
||||
"Editor": "Editor",
|
||||
"Export": "Exportieren",
|
||||
"Export your document, it will be inserted in the selected template.": "Exportieren Sie Ihr Dokument, es wird in die gewählte Vorlage eingefügt.",
|
||||
"Failed to add the member in the document.": "Fehler beim Hinzufügen des Mitglieds zum Dokument.",
|
||||
"Failed to copy link": "Link konnte nicht kopiert werden",
|
||||
"Failed to create the invitation for {{email}}.": "Fehler beim Erstellen der Einladung für {{email}}.",
|
||||
"Find a member to add to the document": "Suchen Sie ein Mitglied, das dem Dokument hinzugefügt werden soll",
|
||||
"Go to bottom": "Gehe nach unten",
|
||||
"If a member is editing, his works can be lost.": "Wenn ein Mitglied editiert, können seine Änderungen verloren gehen.",
|
||||
"Improvement and contact": "Verbesserungen und Kontakt",
|
||||
"Invitation sent to {{email}}.": "Einladung an {{email}} gesendet.",
|
||||
@@ -58,33 +45,24 @@
|
||||
"Link Copied !": "Link kopiert!",
|
||||
"Login": "Anmelden",
|
||||
"Logout": "Abmelden",
|
||||
"Members": "Mitglieder",
|
||||
"No editor found": "Kein Editor gefunden",
|
||||
"Offline ?!": "Offline?!",
|
||||
"Only for people with access": "Nur für Personen mit Zugriff",
|
||||
"Open the document options": "Öffnen Sie die Dokumentoptionen",
|
||||
"Open the panel": "Panel öffnen",
|
||||
"Open the version options": "Öffnen Sie die Versionsoptionen",
|
||||
"Ouch !": "Autsch!",
|
||||
"Owner": "Besitzer",
|
||||
"Owners:": "Besitzer:",
|
||||
"PDF": "PDF",
|
||||
"Personal data and cookies": "Personenbezogene Daten und Cookies",
|
||||
"Public": "Öffentlich",
|
||||
"Read only, you cannot edit document versions.": "Nur lesen: Sie können Dokumentenversionen nicht bearbeiten.",
|
||||
"Read only, you cannot edit this document.": "Nur lesen: Sie können dieses Dokument nicht bearbeiten.",
|
||||
"Reader": "Leser",
|
||||
"Rename": "Umbenennen",
|
||||
"Restore": "Wiederherstellen",
|
||||
"Restore the version": "Version wiederherstellen",
|
||||
"Restore this version": "Version wiederherstellen",
|
||||
"Restore this version?": "Diese Version wiederherstellen?",
|
||||
"Role": "Rolle",
|
||||
"Search by email": "Nach E-Mail suchen",
|
||||
"Share": "Teilen",
|
||||
"Share modal": "Teilen-Modal",
|
||||
"Something bad happens, please retry.": "Etwas ist schiefgelaufen, bitte versuchen Sie es erneut.",
|
||||
"Table of content": "Inhaltsverzeichnis",
|
||||
"Table of contents": "Inhaltsverzeichnis",
|
||||
"Template": "Vorlage",
|
||||
"The document has been deleted.": "Das Dokument wurde gelöscht.",
|
||||
@@ -101,15 +79,11 @@
|
||||
"Validate": "Bestätigen",
|
||||
"Version history": "Versionsverlauf",
|
||||
"Version restored successfully": "Version erfolgreich wiederhergestellt",
|
||||
"Versions": "Versionen",
|
||||
"We didn't find a mail matching, try to be more accurate": "Wir haben keine übereinstimmende E-Mail gefunden, versuchen Sie genauer zu sein",
|
||||
"We try to respond within 2 working days.": "Wir versuchen, innerhalb von 2 Arbeitstagen zu antworten.",
|
||||
"You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.": "Sie sind der einzige Besitzer dieser Gruppe. Machen Sie ein anderes Mitglied zum Gruppenbesitzer, bevor Sie Ihre eigene Rolle ändern oder aus Ihrem Dokument entfernen können.",
|
||||
"You cannot update the role or remove other owner.": "Sie können die Rolle nicht aktualisieren oder einen anderen Besitzer entfernen.",
|
||||
"You don't have any document yet.": "Sie haben noch kein Dokument.",
|
||||
"Your current document will revert to this version.": "Ihr aktuelles Dokument wird auf diese Version zurückgesetzt.",
|
||||
"Your role": "Ihre Rolle",
|
||||
"Your role:": "Ihre Rolle:",
|
||||
"Your {{format}} was downloaded succesfully": "Ihr {{format}} wurde erfolgreich heruntergeladen"
|
||||
}
|
||||
},
|
||||
@@ -124,17 +98,17 @@
|
||||
"Accessibility statement": "Déclaration d'accessibilité",
|
||||
"Address:": "Adresse :",
|
||||
"Administrator": "Administrateur",
|
||||
"All docs": "Tous les documents",
|
||||
"Anonymous": "Anonyme",
|
||||
"Anyone on the internet with the link can view": "Les personnes disposant du lien peuvent y accéder",
|
||||
"Are you sure you want to delete the document \"{{title}}\"?": "Êtes-vous sûr de vouloir supprimer le document \"{{title}}\" ?",
|
||||
"Authenticated": "Authentifié",
|
||||
"Back to home page": "Retour à l'accueil",
|
||||
"Back to top": "Retour en haut",
|
||||
"Can read and edit": "Peut lire et éditer",
|
||||
"Can't load this page, please check your internet connection.": "Impossible de charger cette page, veuillez vérifier votre connexion Internet.",
|
||||
"Cancel": "Annuler",
|
||||
"Choose a role": "Choisissez un rôle",
|
||||
"Close the modal": "Fermer la modale",
|
||||
"Close the panel": "Fermer le panneau",
|
||||
"Compliance status": "État de conformité",
|
||||
"Confirm deletion": "Confirmer la suppression",
|
||||
"Content modal to delete document": "Contenu modal pour supprimer le document",
|
||||
@@ -146,38 +120,28 @@
|
||||
"Copy link": "Copier le lien",
|
||||
"Copyright": "Copyright",
|
||||
"Correct": "Corriger",
|
||||
"Create a new document": "Créer un nouveau document",
|
||||
"Created at": "Créé le",
|
||||
"Current version": "Version en cours",
|
||||
"Datagrid of the documents page {{page}}": "Page {{page}} de la grille des données des documents",
|
||||
"Defender of Rights - Free response - 71120 75342 Paris CEDEX 07": "Défenseur des droits - Réponse gratuite - 71120 75342 Paris CEDEX 07",
|
||||
"Delete document": "Supprimer le document",
|
||||
"Delete the document": "Supprimer le document",
|
||||
"Deleting the document \"{{title}}\"": "Suppression du document \"{{title}}\"",
|
||||
"Doc visibility card": "Carte de visibilité du doc",
|
||||
"Docs": "Docs",
|
||||
"Docs Logo": "Logo Docs",
|
||||
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs : Votre nouveau compagnon pour collaborer sur des documents efficacement, intuitivement et en toute sécurité.",
|
||||
"Document icon": "Icône du document",
|
||||
"Document name": "Nom du document",
|
||||
"Document panel": "Panneau du document",
|
||||
"Document title updated successfully": "Titre du document mis à jour avec succès",
|
||||
"Documents": "Documents",
|
||||
"Docx": "Docx",
|
||||
"Download": "Télécharger",
|
||||
"E-mail:": "E-mail:",
|
||||
"Editor": "Éditeur",
|
||||
"Editor unavailable": "Éditeur indisponible",
|
||||
"Established on December 20, 2023.": "Établi le 20 décembre 2023.",
|
||||
"Export": "Exporter",
|
||||
"Export your document, it will be inserted in the selected template.": "Exportez votre document, il sera inséré dans le modèle sélectionné.",
|
||||
"Failed to add the member in the document.": "Impossible d'ajouter le membre dans le document.",
|
||||
"Failed to copy link": "Échec de la copie du lien",
|
||||
"Failed to copy to clipboard": "Échec de la copie dans le presse-papier",
|
||||
"Failed to create the invitation for {{email}}.": "Impossible de créer l'invitation pour {{email}}.",
|
||||
"Find a member to add to the document": "Trouver un membre à ajouter au document",
|
||||
"Format": "Format",
|
||||
"French Interministerial Directorate for Digital Affairs (DINUM), 20 avenue de Ségur 75007 Paris.": "Direction interministérielle des affaires numériques (DINUM), 20 avenue de Segur 75007 Paris.",
|
||||
"Go to bottom": "Aller en bas",
|
||||
"History": "Historique",
|
||||
"How people can interact with the document": "Comment les gens peuvent interagir avec le document",
|
||||
"If a member is editing, his works can be lost.": "Si un membre est en train d'éditer, ses travaux peuvent être perdus.",
|
||||
"If you are unable to access a content or a service, you can contact the person responsible for https://lasuite.numerique.gouv.fr to be directed to an accessible alternative or to obtain the content in another form.": "Si vous ne pouvez pas accéder à un contenu ou à un service, vous pouvez contacter la personne responsable de https://lasuite. umerique.gouv.fr pour être dirigé vers une alternative accessible ou pour obtenir le contenu sous une autre forme.",
|
||||
@@ -187,57 +151,61 @@
|
||||
"Invite new members to {{title}}": "Invitez de nouveaux membres à rejoindre {{title}}",
|
||||
"Invited": "Invité",
|
||||
"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.",
|
||||
"It's true, you didn't have to click on a block that covers half the page to say you agree to the placement of cookies — even if you don't know what it means!": "C'est vrai, vous n'avez pas à cliquer sur un bloc qui couvre la moitié de la page pour dire que vous acceptez le placement de cookies — même si vous ne savez pas ce que cela signifie !",
|
||||
"Language": "Langue",
|
||||
"Last update: {{update}}": "Dernière mise à jour : {{update}}",
|
||||
"Legal Notice": "Mentions Legales",
|
||||
"Legal notice": "Mention légale",
|
||||
"Link Copied !": "Lien copié !",
|
||||
"List invitation card": "Carte de liste d'invitation",
|
||||
"List members card": "Carte liste des membres",
|
||||
"Load more": "Afficher plus",
|
||||
"Login": "Connexion",
|
||||
"Logout": "Se déconnecter",
|
||||
"Members": "Membres",
|
||||
"Modal confirmation to restore the version": "Modale de confirmation pour restaurer la version",
|
||||
"More docs": "Plus de documents",
|
||||
"More info?": "Plus d'infos ?",
|
||||
"My docs": "Mes documents",
|
||||
"Name": "Nom",
|
||||
"New doc": "Nouveau doc",
|
||||
"No editor found": "Pas d'éditeur trouvé",
|
||||
"No versions": "Aucune version",
|
||||
"Nothing exceptional, no special privileges related to a .gouv.fr.": "Rien d'exceptionnel, pas de privilèges spéciaux liés à un .gouv.fr.",
|
||||
"Offline ?!": "Hors-ligne ?!",
|
||||
"Only for authenticated users": "Uniquement pour les utilisateurs authentifiés",
|
||||
"Only for people with access": "Seulement pour les personnes avec accès",
|
||||
"Open the document options": "Ouvrir les options du document",
|
||||
"Open the header menu": "Ouvrir le menu d'en-tête",
|
||||
"Open the panel": "Ouvrir le panneau",
|
||||
"Open the version options": "Ouvrir les options de version",
|
||||
"Ouch !": "Aïe !",
|
||||
"Owner": "Propriétaire",
|
||||
"Owners:": "Propriétaires:",
|
||||
"PDF": "PDF",
|
||||
"Personal data and cookies": "Données personnelles et cookies",
|
||||
"Public": "Public",
|
||||
"Public document": "Document public",
|
||||
"Publication Director": "Directeur de la publication",
|
||||
"Publisher": "Éditeur",
|
||||
"Read only": "Lecture seule",
|
||||
"Read only, you cannot edit document versions.": "En lecture seule, vous ne pouvez pas éditer les versions du document.",
|
||||
"Read only, you cannot edit this document.": "En lecture seule, vous ne pouvez pas éditer ce document.",
|
||||
"Reader": "Lecteur",
|
||||
"Remedies": "Voie de recours",
|
||||
"Remove": "Supprimer",
|
||||
"Rename": "Renommer",
|
||||
"Rephrase": "Reformuler",
|
||||
"Restore": "Restaurer",
|
||||
"Restore the version": "Restaurer la version",
|
||||
"Restore this version": "Restaurer cette version",
|
||||
"Restore this version?": "Restaurer cette version?",
|
||||
"Restricted": "Restreint",
|
||||
"Role": "Rôle",
|
||||
"Search by email": "Recherche par email",
|
||||
"Select a version on the right to restore": "Sélectionnez une version à droite à restaurer",
|
||||
"Send a letter by post (free of charge, no stamp needed):": "Envoyer un courrier par la poste (gratuit, ne pas mettre de timbre):",
|
||||
"Share": "Partager",
|
||||
"Share modal": "Modale de partage",
|
||||
"Shared with me": "Partagés avec moi",
|
||||
"Something bad happens, please retry.": "Une erreur inattendue s'est produite, veuillez réessayer.",
|
||||
"Stéphanie Schaer: Interministerial Digital Director (DINUM).": "Stéphanie Schaer: Directrice numérique interministériel (DINUM).",
|
||||
"Summarize": "Résumer",
|
||||
"Table of content": "Table des matières",
|
||||
"Summary": "Sommaire",
|
||||
"Table of contents": "Table des matières",
|
||||
"Template": "Template",
|
||||
"The document has been deleted.": "Le document a bien été supprimé.",
|
||||
@@ -257,24 +225,23 @@
|
||||
"Unless otherwise stated, all content on this site is under": "Sauf mention contraire, tout le contenu de ce site est sous",
|
||||
"Untitled document": "Document sans titre",
|
||||
"Updated at": "Mise à jour le",
|
||||
"Upload your docs to a Microsoft Word, Open Office or PDF document.": "Téléchargez vos documents dans un document Microsoft Word, Open Office ou PDF.",
|
||||
"Use as prompt": "Utiliser comme un prompt",
|
||||
"User {{email}} added to the document.": "L'utilisateur {{email}} a été ajouté au document.",
|
||||
"Validate": "Valider",
|
||||
"Version history": "Historique des versions",
|
||||
"Version restored successfully": "Version restaurée avec succès",
|
||||
"Versions": "Versions",
|
||||
"Visibility": "Visibilité",
|
||||
"Warning": "Attention",
|
||||
"We didn't find a mail matching, try to be more accurate": "Nous n'avons pas trouvé de correspondance par mail, essayez d'être plus précis",
|
||||
"We simply comply with the law, which states that certain audience measurement tools, properly configured to respect privacy, are exempt from prior authorization.": "Nous nous conformons simplement à la loi, qui stipule que certains outils de mesure d’audience, correctement configurés pour respecter la vie privée, sont exemptés de toute autorisation préalable.",
|
||||
"We try to respond within 2 working days.": "Nous essayons de répondre dans les 2 jours ouvrables.",
|
||||
"Word / Open Office": "Word / Open Office",
|
||||
"You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.": "Vous êtes le seul propriétaire de ce groupe, faites d'un autre membre le propriétaire du groupe, avant de pouvoir modifier votre propre rôle ou vous supprimer du document.",
|
||||
"You can oppose the tracking of your browsing on this website.": "Vous pouvez vous opposer au suivi de votre navigation sur ce site.",
|
||||
"You can:": "Vous pouvez:",
|
||||
"You cannot update the role or remove other owner.": "Vous ne pouvez pas mettre à jour le rôle ou supprimer un autre propriétaire.",
|
||||
"You don't have any document yet.": "Vous n'avez pas encore de document.",
|
||||
"Your current document will revert to this version.": "Votre document actuel va revenir à cette version.",
|
||||
"Your role": "Votre rôle",
|
||||
"Your role:": "Votre rôle:",
|
||||
"Your {{format}} was downloaded succesfully": "Votre {{format}} a été téléchargé avec succès",
|
||||
"accessibility-contact-defenseurdesdroits": "Contacter le délégué du<1>Défenseur des droits dans votre région</1>",
|
||||
"accessibility-dinum-services": "<strong>DINUM</strong> s'engage à rendre accessibles ses services numériques, conformément à l'article 47 de la loi n° 2005-102 du 11 février 2005.",
|
||||
|
||||
@@ -1,36 +1,64 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Footer } from '@/features/footer';
|
||||
import { Header } from '@/features/header';
|
||||
import { HEADER_HEIGHT } from '@/features/header/conf';
|
||||
import { LeftPanel } from '@/features/left-panel';
|
||||
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
interface MainLayoutProps {
|
||||
type MainLayoutProps = {
|
||||
backgroundColor?: 'white' | 'grey';
|
||||
withoutFooter?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export function MainLayout({
|
||||
children,
|
||||
withoutFooter,
|
||||
backgroundColor = 'white',
|
||||
withoutFooter = false,
|
||||
}: PropsWithChildren<MainLayoutProps>) {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const colors = colorsTokens();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box $minHeight="100vh">
|
||||
<Header />
|
||||
<Box $css="flex: 1;" $direction="row">
|
||||
<Box
|
||||
as="main"
|
||||
$minHeight="100vh"
|
||||
$width="100%"
|
||||
$background={colorsTokens()['primary-bg']}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
<div>
|
||||
<Header />
|
||||
<Box
|
||||
$direction="row"
|
||||
$margin={{ top: `${HEADER_HEIGHT}px` }}
|
||||
$width="100%"
|
||||
>
|
||||
<LeftPanel />
|
||||
<Box
|
||||
as="main"
|
||||
id={MAIN_LAYOUT_ID}
|
||||
$align="center"
|
||||
$flex={1}
|
||||
$width="100%"
|
||||
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
|
||||
$padding={{
|
||||
vertical: isDesktop ? 'base' : 'xs',
|
||||
horizontal: isDesktop ? 'base' : 'xs',
|
||||
}}
|
||||
$background={
|
||||
backgroundColor === 'white'
|
||||
? colors['greyscale-000']
|
||||
: colors['greyscale-050']
|
||||
}
|
||||
$css={css`
|
||||
overflow-y: auto;
|
||||
overflow-x: clip;
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
{!withoutFooter && <Footer />}
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
1
src/frontend/apps/impress/src/layouts/conf.ts
Normal file
1
src/frontend/apps/impress/src/layouts/conf.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const MAIN_LAYOUT_ID = `mainContent`;
|
||||
@@ -28,6 +28,7 @@ export function DocLayout() {
|
||||
<Head>
|
||||
<meta name="robots" content="noindex" />
|
||||
</Head>
|
||||
|
||||
<MainLayout withoutFooter>
|
||||
<DocPage id={id} />
|
||||
</MainLayout>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
import { DocLayout } from '../index';
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout() {
|
||||
return <DocLayout />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,13 +0,0 @@
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
import { DocLayout } from '../index';
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout() {
|
||||
return <DocLayout />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,15 +1,25 @@
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import { DocsGridContainer } from '@/features/docs/docs-grid';
|
||||
import { Box } from '@/components';
|
||||
import { DocDefaultFilter } from '@/features/docs';
|
||||
import { DocsGrid } from '@/features/docs/docs-grid/components/DocsGrid';
|
||||
import { MainLayout } from '@/layouts';
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
return <DocsGridContainer />;
|
||||
const searchParams = useSearchParams();
|
||||
const target = searchParams.get('target');
|
||||
|
||||
return (
|
||||
<Box $width="100%" $align="center">
|
||||
<DocsGrid target={target as DocDefaultFilter} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
return <MainLayout>{page}</MainLayout>;
|
||||
return <MainLayout backgroundColor="grey">{page}</MainLayout>;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -4,41 +4,67 @@ export type ScreenSize = 'small-mobile' | 'mobile' | 'tablet' | 'desktop';
|
||||
|
||||
export interface UseResponsiveStore {
|
||||
isMobile: boolean;
|
||||
isTablet: boolean;
|
||||
isSmallMobile: boolean;
|
||||
screenSize: ScreenSize;
|
||||
screenWidth: number;
|
||||
setScreenSize: (size: ScreenSize) => void;
|
||||
isDesktop: boolean;
|
||||
initializeResizeListener: () => () => void;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
isMobile: false,
|
||||
isSmallMobile: false,
|
||||
isTablet: false,
|
||||
isDesktop: false,
|
||||
screenSize: 'desktop' as ScreenSize,
|
||||
screenWidth: 0,
|
||||
};
|
||||
|
||||
export const useResponsiveStore = create<UseResponsiveStore>((set) => ({
|
||||
isMobile: initialState.isMobile,
|
||||
isTablet: initialState.isTablet,
|
||||
isSmallMobile: initialState.isSmallMobile,
|
||||
screenSize: initialState.screenSize,
|
||||
screenWidth: initialState.screenWidth,
|
||||
setScreenSize: (size: ScreenSize) => set(() => ({ screenSize: size })),
|
||||
isDesktop: initialState.isDesktop,
|
||||
initializeResizeListener: () => {
|
||||
const resizeHandler = () => {
|
||||
const width = window.innerWidth;
|
||||
if (width < 560) {
|
||||
set({
|
||||
isDesktop: false,
|
||||
screenSize: 'small-mobile',
|
||||
isMobile: true,
|
||||
isTablet: false,
|
||||
isSmallMobile: true,
|
||||
});
|
||||
} else if (width < 768) {
|
||||
set({ screenSize: 'mobile', isMobile: true, isSmallMobile: false });
|
||||
set({
|
||||
isDesktop: false,
|
||||
screenSize: 'mobile',
|
||||
isTablet: false,
|
||||
isMobile: true,
|
||||
isSmallMobile: false,
|
||||
});
|
||||
} else if (width >= 768 && width < 1024) {
|
||||
set({ screenSize: 'tablet', isMobile: false, isSmallMobile: false });
|
||||
set({
|
||||
isDesktop: false,
|
||||
screenSize: 'tablet',
|
||||
isTablet: true,
|
||||
isMobile: false,
|
||||
isSmallMobile: false,
|
||||
});
|
||||
} else {
|
||||
set({ screenSize: 'desktop', isMobile: false, isSmallMobile: false });
|
||||
set({
|
||||
isDesktop: true,
|
||||
screenSize: 'desktop',
|
||||
isTablet: false,
|
||||
isMobile: false,
|
||||
isSmallMobile: false,
|
||||
});
|
||||
}
|
||||
|
||||
set({ screenWidth: width });
|
||||
|
||||
@@ -10713,6 +10713,11 @@ react-icons@^5.2.1:
|
||||
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.3.0.tgz#ccad07a30aebd40a89f8cfa7d82e466019203f1c"
|
||||
integrity sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==
|
||||
|
||||
react-intersection-observer@9.13.1:
|
||||
version "9.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.13.1.tgz#6c61a75801162491c6348bad09967f2caf445584"
|
||||
integrity sha512-tSzDaTy0qwNPLJHg8XZhlyHTgGW6drFKTtvjdL+p6um12rcnp8Z5XstE+QNBJ7c64n5o0Lj4ilUleA41bmDoMw==
|
||||
|
||||
react-is@18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||
|
||||
Reference in New Issue
Block a user