Compare commits
19 Commits
v1.1.0
...
v1.2.0-preprod
| Author | SHA1 | Date | |
|---|---|---|---|
| 50dfb53a17 | |||
| 6eac20da61 | |||
| c42fd9be9c | |||
| 2b824c0862 | |||
| fb494c8c71 | |||
| 8d5648005f | |||
| 713d9e48c8 | |||
| bef541f956 | |||
| 4b61ffce01 | |||
| a9383212a3 | |||
| 1ed20c3896 | |||
| b5443676c1 | |||
| 424c100eeb | |||
| 91be4f5a21 | |||
| 6eb0ac99e4 | |||
| e79a74083a | |||
| 905000550d | |||
| 76d623a89b | |||
| 61cf8aae74 |
+21
-1
@@ -8,6 +8,24 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.2.0] - 2024-08-06
|
||||
|
||||
## Added
|
||||
|
||||
- 🎨(frontend) better conversion editor to pdf #151
|
||||
- ✨(frontend) Versioning #147
|
||||
|
||||
## Fixed
|
||||
|
||||
- 🐛(y-webrtc) fix prob connection #147
|
||||
- ⚡️(frontend) improve select share stability #159
|
||||
|
||||
## Changed
|
||||
|
||||
- 🎨(frontend) stop limit layout height to screen size #158
|
||||
|
||||
## [1.1.0] - 2024-07-15
|
||||
|
||||
## Added
|
||||
|
||||
- 🤡(demo) generate dummy documents on dev users #120
|
||||
@@ -67,6 +85,8 @@ and this project adheres to
|
||||
- 🚀 Impress, project to manage your documents easily and collaboratively.
|
||||
|
||||
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v1.0.0...main
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v1.2.0...main
|
||||
[1.2.0]: https://github.com/numerique-gouv/impress/releases/v1.2.0
|
||||
[1.1.0]: https://github.com/numerique-gouv/impress/releases/v1.1.0
|
||||
[1.0.0]: https://github.com/numerique-gouv/impress/releases/v1.0.0
|
||||
[0.1.0]: https://github.com/numerique-gouv/impress/releases/v0.1.0
|
||||
@@ -259,3 +259,12 @@ class InvitationSerializer(serializers.ModelSerializer):
|
||||
attrs["document_id"] = document_id
|
||||
attrs["issuer"] = user
|
||||
return attrs
|
||||
|
||||
|
||||
class DocumentVersionSerializer(serializers.Serializer):
|
||||
"""Serialize Versions."""
|
||||
|
||||
etag = serializers.CharField()
|
||||
is_latest = serializers.BooleanField()
|
||||
last_modified = serializers.DateTimeField()
|
||||
version_id = serializers.CharField()
|
||||
|
||||
@@ -337,10 +337,18 @@ class DocumentViewSet(
|
||||
Q(user=request.user) | Q(team__in=request.user.get_teams()),
|
||||
)
|
||||
)
|
||||
return drf_response.Response(
|
||||
document.get_versions_slice(from_datetime=from_datetime)
|
||||
|
||||
versions_data = document.get_versions_slice(from_datetime=from_datetime)[
|
||||
"versions"
|
||||
]
|
||||
paginator = pagination.PageNumberPagination()
|
||||
paginated_versions = paginator.paginate_queryset(versions_data, request)
|
||||
serialized_versions = serializers.DocumentVersionSerializer(
|
||||
paginated_versions, many=True
|
||||
)
|
||||
|
||||
return paginator.get_paginated_response(serialized_versions.data)
|
||||
|
||||
@decorators.action(
|
||||
detail=True,
|
||||
methods=["get", "delete"],
|
||||
@@ -377,6 +385,7 @@ class DocumentViewSet(
|
||||
{
|
||||
"content": response["Body"].read().decode("utf-8"),
|
||||
"last_modified": response["LastModified"],
|
||||
"id": version_id,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -125,7 +125,8 @@ def test_api_document_versions_list_authenticated_related(via, mock_user_get_tea
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content["versions"]) == 0
|
||||
assert len(content["results"]) == 0
|
||||
assert content["count"] == 0
|
||||
|
||||
# Add a new version to the document
|
||||
document.content = "new content"
|
||||
@@ -137,9 +138,8 @@ def test_api_document_versions_list_authenticated_related(via, mock_user_get_tea
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content["versions"]) == 1
|
||||
assert content["next_version_id_marker"] == ""
|
||||
assert content["is_truncated"] is False
|
||||
assert len(content["results"]) == 1
|
||||
assert content["count"] == 1
|
||||
|
||||
|
||||
def test_api_document_versions_retrieve_anonymous_public():
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "1.0.0"
|
||||
version = "1.2.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -51,7 +51,7 @@ dependencies = [
|
||||
"PyJWT==2.8.0",
|
||||
"python-frontmatter==1.0.1",
|
||||
"requests==2.32.2",
|
||||
"sentry-sdk==1.38.0",
|
||||
"sentry-sdk==2.8.0",
|
||||
"url-normalize==1.4.3",
|
||||
"WeasyPrint>=60.2",
|
||||
"whitenoise==6.6.0",
|
||||
|
||||
@@ -140,3 +140,34 @@ export const goToGridDoc = async (
|
||||
|
||||
return docTitle as string;
|
||||
};
|
||||
|
||||
export const mockedDocument = async (page: Page, json: object) => {
|
||||
await page.route('**/documents/**/', async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method().includes('GET') && !request.url().includes('page=')) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
content: '',
|
||||
title: 'Mocked document',
|
||||
accesses: [],
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: false,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: false, // Means not admin
|
||||
update: false,
|
||||
partial_update: false, // Means not editor
|
||||
retrieve: true,
|
||||
},
|
||||
is_public: false,
|
||||
created_at: '2021-09-01T09:00:00Z',
|
||||
...json,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, goToGridDoc } from './common';
|
||||
import { createDoc, goToGridDoc, mockedDocument } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -153,42 +153,23 @@ test.describe('Doc Editor', () => {
|
||||
});
|
||||
|
||||
test('it cannot edit if viewer', async ({ page }) => {
|
||||
await page.route('**/documents/**/', async (route) => {
|
||||
const request = route.request();
|
||||
if (
|
||||
request.method().includes('GET') &&
|
||||
!request.url().includes('page=')
|
||||
) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
content: '',
|
||||
title: 'Mocked document',
|
||||
accesses: [],
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: false,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: false, // Means not admin
|
||||
update: false,
|
||||
partial_update: false, // Means not editor
|
||||
retrieve: true,
|
||||
},
|
||||
is_public: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
await mockedDocument(page, {
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: false,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: false, // Means not admin
|
||||
update: false,
|
||||
partial_update: false, // Means not editor
|
||||
retrieve: true,
|
||||
},
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(
|
||||
page.getByText(
|
||||
"Read only, you don't have the right to update this document.",
|
||||
),
|
||||
page.getByText('Read only, you cannot edit this document.'),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ test.describe('Documents Grid', () => {
|
||||
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(/Users number/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();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { goToGridDoc } from './common';
|
||||
import { goToGridDoc, mockedDocument } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -8,57 +8,42 @@ test.beforeEach(async ({ page }) => {
|
||||
|
||||
test.describe('Doc Header', () => {
|
||||
test('it checks the element are correctly displayed', async ({ page }) => {
|
||||
await page.route('**/documents/**/', async (route) => {
|
||||
const request = route.request();
|
||||
if (
|
||||
request.method().includes('GET') &&
|
||||
!request.url().includes('page=')
|
||||
) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
content: '',
|
||||
title: 'Mocked document',
|
||||
accesses: [
|
||||
{
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
role: 'owner',
|
||||
user: {
|
||||
email: 'super@owner.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
role: 'admin',
|
||||
user: {
|
||||
email: 'super@admin.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
role: 'owner',
|
||||
user: {
|
||||
email: 'super2@owner.com',
|
||||
},
|
||||
},
|
||||
],
|
||||
abilities: {
|
||||
destroy: true, // Means owner
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: true,
|
||||
update: true,
|
||||
partial_update: true,
|
||||
retrieve: true,
|
||||
},
|
||||
is_public: true,
|
||||
created_at: '2021-09-01T09:00:00Z',
|
||||
await mockedDocument(page, {
|
||||
accesses: [
|
||||
{
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
role: 'owner',
|
||||
user: {
|
||||
email: 'super@owner.com',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
role: 'admin',
|
||||
user: {
|
||||
email: 'super@admin.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
role: 'owner',
|
||||
user: {
|
||||
email: 'super2@owner.com',
|
||||
},
|
||||
},
|
||||
],
|
||||
abilities: {
|
||||
destroy: true, // Means owner
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: true,
|
||||
update: true,
|
||||
partial_update: true,
|
||||
retrieve: true,
|
||||
},
|
||||
is_public: true,
|
||||
created_at: '2021-09-01T09:00:00Z',
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
@@ -170,7 +170,7 @@ test.describe('Document create member', () => {
|
||||
|
||||
const inputSearch = page.getByLabel(/Find a member to add to the document/);
|
||||
|
||||
const email = randomName('test@test.fr', browserName, 1)[0];
|
||||
const [email] = randomName('test@test.fr', browserName, 1);
|
||||
await inputSearch.fill(email);
|
||||
await page.getByRole('option', { name: email }).click();
|
||||
|
||||
@@ -191,7 +191,22 @@ test.describe('Document create member', () => {
|
||||
expect(responseCreateInvitation.ok()).toBeTruthy();
|
||||
|
||||
await inputSearch.fill(email);
|
||||
await expect(page.getByText('Loading...')).toBeHidden();
|
||||
await expect(page.getByRole('option', { name: email })).toBeHidden();
|
||||
await page.getByRole('option', { name: email }).click();
|
||||
// Choose a role
|
||||
await page.getByRole('combobox', { name: /Choose a role/ }).click();
|
||||
await page.getByRole('option', { name: 'Owner' }).click();
|
||||
|
||||
const responsePromiseCreateInvitationFail = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/invitations/') && response.status() === 400,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Validate' }).click();
|
||||
await expect(
|
||||
page.getByText(`"${email}" is already invited to the document.`),
|
||||
).toBeVisible();
|
||||
const responseCreateInvitationFail =
|
||||
await responsePromiseCreateInvitationFail;
|
||||
expect(responseCreateInvitationFail.ok()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
|
||||
import cs from 'convert-stream';
|
||||
import pdf from 'pdf-parse';
|
||||
|
||||
import { createDoc, goToGridDoc } from './common';
|
||||
import { createDoc, goToGridDoc, mockedDocument } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -67,6 +67,21 @@ test.describe('Doc Tools', () => {
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.locator('.bn-block-outer').last().fill('Break');
|
||||
await expect(page.getByText('Break')).toBeVisible();
|
||||
|
||||
// Center the text
|
||||
await page.getByText('Break').dblclick();
|
||||
await page.locator('button[data-test="alignTextCenter"]').click();
|
||||
|
||||
// Change the background color
|
||||
await page.getByText('Break').dblclick();
|
||||
await page.locator('button[data-test="colors"]').click();
|
||||
await page.locator('button[data-test="background-color-brown"]').click();
|
||||
|
||||
// Change the text color
|
||||
await page.getByText('Break').dblclick();
|
||||
await page.locator('button[data-test="colors"]').click();
|
||||
await page.locator('button[data-test="text-color-orange"]').click();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
@@ -82,7 +97,10 @@ test.describe('Doc Tools', () => {
|
||||
.click();
|
||||
|
||||
// Empty paragraph should be replaced by a <br/>
|
||||
expect(body).toContain('<br/><br/>');
|
||||
expect(body.match(/<br\/>/g)?.length).toBeGreaterThanOrEqual(2);
|
||||
expect(body).toContain('style="color: orange;"');
|
||||
expect(body).toContain('style="text-align: center;"');
|
||||
expect(body).toContain('style="background-color: brown;"');
|
||||
});
|
||||
|
||||
test('it updates the doc', async ({ page, browserName }) => {
|
||||
@@ -182,34 +200,17 @@ test.describe('Doc Tools', () => {
|
||||
});
|
||||
|
||||
test('it checks the options available if administrator', async ({ page }) => {
|
||||
await page.route('**/documents/**/', async (route) => {
|
||||
const request = route.request();
|
||||
if (
|
||||
request.method().includes('GET') &&
|
||||
!request.url().includes('page=')
|
||||
) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
content: '',
|
||||
title: 'Mocked document',
|
||||
accesses: [],
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: true, // Means admin
|
||||
update: true,
|
||||
partial_update: true,
|
||||
retrieve: true,
|
||||
},
|
||||
is_public: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
await mockedDocument(page, {
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: true, // Means admin
|
||||
update: true,
|
||||
partial_update: true,
|
||||
retrieve: true,
|
||||
},
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
@@ -232,34 +233,17 @@ test.describe('Doc Tools', () => {
|
||||
});
|
||||
|
||||
test('it checks the options available if editor', async ({ page }) => {
|
||||
await page.route('**/documents/**/', async (route) => {
|
||||
const request = route.request();
|
||||
if (
|
||||
request.method().includes('GET') &&
|
||||
!request.url().includes('page=')
|
||||
) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
content: '',
|
||||
title: 'Mocked document',
|
||||
accesses: [],
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: false, // Means not admin
|
||||
update: true,
|
||||
partial_update: true, // Means editor
|
||||
retrieve: true,
|
||||
},
|
||||
is_public: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
await mockedDocument(page, {
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: false, // Means not admin
|
||||
update: true,
|
||||
partial_update: true, // Means editor
|
||||
retrieve: true,
|
||||
},
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
@@ -282,34 +266,17 @@ test.describe('Doc Tools', () => {
|
||||
});
|
||||
|
||||
test('it checks the options available if reader', async ({ page }) => {
|
||||
await page.route('**/documents/**/', async (route) => {
|
||||
const request = route.request();
|
||||
if (
|
||||
request.method().includes('GET') &&
|
||||
!request.url().includes('page=')
|
||||
) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
content: '',
|
||||
title: 'Mocked document',
|
||||
accesses: [],
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: false,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: false, // Means not admin
|
||||
update: false,
|
||||
partial_update: false, // Means not editor
|
||||
retrieve: true,
|
||||
},
|
||||
is_public: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
await mockedDocument(page, {
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: false,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: false, // Means not admin
|
||||
update: false,
|
||||
partial_update: false, // Means not editor
|
||||
retrieve: true,
|
||||
},
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, goToGridDoc, mockedDocument } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
const panel = page.getByLabel('Document version panel');
|
||||
|
||||
await expect(panel.getByText('Current version')).toBeVisible();
|
||||
expect(await panel.locator('li').count()).toBe(1);
|
||||
|
||||
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 page
|
||||
.locator('.ProseMirror.bn-editor')
|
||||
.last()
|
||||
.fill('It will create a version');
|
||||
|
||||
await goToGridDoc(page, {
|
||||
title: randomDoc,
|
||||
});
|
||||
|
||||
await expect(page.getByText('Hello World')).toBeHidden();
|
||||
await expect(page.getByText('It will create a version')).toBeVisible();
|
||||
|
||||
await expect(panel.getByText('Current version')).toBeVisible();
|
||||
expect(await panel.locator('li').count()).toBe(2);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
test('it does not display the doc versions if not allowed', async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockedDocument(page, {
|
||||
abilities: {
|
||||
versions_list: false,
|
||||
partial_update: true,
|
||||
},
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
|
||||
|
||||
await expect(page.getByLabel('Document version panel')).toBeHidden();
|
||||
});
|
||||
|
||||
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 page.locator('.bn-block-outer').last().click();
|
||||
await page.locator('.bn-block-outer').last().fill('Hello');
|
||||
|
||||
await goToGridDoc(page, {
|
||||
title: randomDoc,
|
||||
});
|
||||
|
||||
await expect(page.getByText('Hello')).toBeVisible();
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
await page.keyboard.press('Enter');
|
||||
await page.locator('.bn-block-outer').last().fill('World');
|
||||
|
||||
await goToGridDoc(page, {
|
||||
title: randomDoc,
|
||||
});
|
||||
|
||||
await expect(page.getByText('World')).toBeVisible();
|
||||
|
||||
const panel = page.getByLabel('Document version panel');
|
||||
await panel.locator('li').nth(1).click();
|
||||
await expect(page.getByText('World')).toBeHidden();
|
||||
|
||||
await panel.getByLabel('Open the version options').click();
|
||||
await page.getByText('Restore the version').click();
|
||||
|
||||
await expect(panel.locator('li')).toHaveCount(3);
|
||||
|
||||
await panel.getByText('Current version').click();
|
||||
await expect(page.getByText('Hello')).toBeVisible();
|
||||
await expect(page.getByText('World')).toBeHidden();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
@@ -9,7 +9,7 @@
|
||||
"test:ui": "yarn test --ui"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.45.0",
|
||||
"@playwright/test": "1.45.3",
|
||||
"@types/node": "*",
|
||||
"@types/pdf-parse": "1.1.4",
|
||||
"eslint-config-impress": "*",
|
||||
|
||||
@@ -8,6 +8,10 @@ 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]/;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -18,33 +18,33 @@
|
||||
"@blocknote/core": "*",
|
||||
"@blocknote/mantine": "*",
|
||||
"@blocknote/react": "*",
|
||||
"@gouvfr-lasuite/integration": "1.0.1",
|
||||
"@gouvfr-lasuite/integration": "1.0.2",
|
||||
"@openfun/cunningham-react": "2.9.3",
|
||||
"@tanstack/react-query": "5.48.0",
|
||||
"i18next": "23.11.5",
|
||||
"@tanstack/react-query": "5.51.16",
|
||||
"i18next": "23.12.2",
|
||||
"idb": "8.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.4.4",
|
||||
"next": "14.2.4",
|
||||
"next": "14.2.5",
|
||||
"react": "*",
|
||||
"react-aria-components": "1.2.1",
|
||||
"react-aria-components": "1.3.1",
|
||||
"react-dom": "*",
|
||||
"react-i18next": "14.1.2",
|
||||
"react-i18next": "15.0.0",
|
||||
"react-select": "5.8.0",
|
||||
"styled-components": "6.1.11",
|
||||
"styled-components": "6.1.12",
|
||||
"y-webrtc": "10.3.0",
|
||||
"yjs": "*",
|
||||
"zustand": "4.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@tanstack/react-query-devtools": "5.48.0",
|
||||
"@testing-library/dom": "10.2.0",
|
||||
"@testing-library/jest-dom": "6.4.6",
|
||||
"@tanstack/react-query-devtools": "5.51.16",
|
||||
"@testing-library/dom": "10.4.0",
|
||||
"@testing-library/jest-dom": "6.4.8",
|
||||
"@testing-library/react": "16.0.0",
|
||||
"@testing-library/user-event": "14.5.2",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/lodash": "4.17.5",
|
||||
"@types/lodash": "4.17.7",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/node": "*",
|
||||
"@types/react": "18.3.3",
|
||||
@@ -56,10 +56,10 @@
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"node-fetch": "2.7.0",
|
||||
"prettier": "3.3.2",
|
||||
"stylelint": "16.6.1",
|
||||
"prettier": "3.3.3",
|
||||
"stylelint": "16.8.1",
|
||||
"stylelint-config-standard": "36.0.1",
|
||||
"stylelint-prettier": "5.0.0",
|
||||
"stylelint-prettier": "5.0.2",
|
||||
"typescript": "*",
|
||||
"workbox-webpack-plugin": "7.1.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
DefinedInitialDataInfiniteOptions,
|
||||
InfiniteData,
|
||||
QueryKey,
|
||||
UseQueryOptions,
|
||||
useInfiniteQuery,
|
||||
} from '@tanstack/react-query';
|
||||
|
||||
import { APIError } from './APIError';
|
||||
import { APIList } from './types';
|
||||
|
||||
export type UseQueryOptionsAPI<Q> = UseQueryOptions<Q, APIError, Q>;
|
||||
export type DefinedInitialDataInfiniteOptionsAPI<Q> =
|
||||
DefinedInitialDataInfiniteOptions<
|
||||
Q,
|
||||
APIError,
|
||||
InfiniteData<Q>,
|
||||
QueryKey,
|
||||
number
|
||||
>;
|
||||
|
||||
/**
|
||||
* @param param Used for infinite scroll pagination
|
||||
* @param queryConfig
|
||||
* @returns
|
||||
*/
|
||||
export const useAPIInfiniteQuery = <T, Q extends { next?: APIList<Q>['next'] }>(
|
||||
key: string,
|
||||
api: (props: T & { page: number }) => Promise<Q>,
|
||||
param: T,
|
||||
queryConfig?: DefinedInitialDataInfiniteOptionsAPI<Q>,
|
||||
) => {
|
||||
return useInfiniteQuery<Q, APIError, InfiniteData<Q>, QueryKey, number>({
|
||||
initialPageParam: 1,
|
||||
queryKey: [key, param],
|
||||
queryFn: ({ pageParam }) =>
|
||||
api({
|
||||
...param,
|
||||
page: pageParam,
|
||||
}),
|
||||
getNextPageParam(lastPage, allPages) {
|
||||
return lastPage.next ? allPages.length + 1 : undefined;
|
||||
},
|
||||
...queryConfig,
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './APIError';
|
||||
export * from './fetchApi';
|
||||
export * from './helpers';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
|
||||
+27
-11
@@ -8,6 +8,7 @@ import { WebrtcProvider } from 'y-webrtc';
|
||||
import { Box } from '@/components';
|
||||
import { useAuthStore } from '@/core/auth';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import { Version } from '@/features/docs/doc-versioning/';
|
||||
|
||||
import useSaveDoc from '../hook/useSaveDoc';
|
||||
import { useDocStore } from '../stores';
|
||||
@@ -17,31 +18,46 @@ import { BlockNoteToolbar } from './BlockNoteToolbar';
|
||||
|
||||
interface BlockNoteEditorProps {
|
||||
doc: Doc;
|
||||
version?: Version;
|
||||
}
|
||||
|
||||
export const BlockNoteEditor = ({ doc }: BlockNoteEditorProps) => {
|
||||
export const BlockNoteEditor = ({ doc, version }: BlockNoteEditorProps) => {
|
||||
const { createProvider, docsStore } = useDocStore();
|
||||
const provider = docsStore?.[doc.id]?.provider;
|
||||
const storeId = version?.id || doc.id;
|
||||
const initialContent = version?.content || doc.content;
|
||||
const provider = docsStore?.[storeId]?.provider;
|
||||
|
||||
useEffect(() => {
|
||||
if (!provider || provider.doc.guid !== storeId) {
|
||||
createProvider(storeId, initialContent);
|
||||
}
|
||||
}, [createProvider, initialContent, provider, storeId]);
|
||||
|
||||
if (!provider) {
|
||||
createProvider(doc.id, doc.content);
|
||||
return null;
|
||||
}
|
||||
|
||||
return <BlockNoteContent doc={doc} provider={provider} />;
|
||||
return <BlockNoteContent doc={doc} provider={provider} storeId={storeId} />;
|
||||
};
|
||||
|
||||
interface BlockNoteContentProps {
|
||||
doc: Doc;
|
||||
provider: WebrtcProvider;
|
||||
storeId: string;
|
||||
}
|
||||
|
||||
export const BlockNoteContent = ({ doc, provider }: BlockNoteContentProps) => {
|
||||
export const BlockNoteContent = ({
|
||||
doc,
|
||||
provider,
|
||||
storeId,
|
||||
}: BlockNoteContentProps) => {
|
||||
const isVersion = doc.id !== storeId;
|
||||
const { userData } = useAuthStore();
|
||||
const { setEditor, docsStore } = useDocStore();
|
||||
useSaveDoc(doc.id, provider.doc, doc.abilities.partial_update);
|
||||
const { setStore, docsStore } = useDocStore();
|
||||
const canSave = doc.abilities.partial_update && !isVersion;
|
||||
useSaveDoc(doc.id, provider.doc, canSave);
|
||||
|
||||
const storedEditor = docsStore?.[doc.id]?.editor;
|
||||
const storedEditor = docsStore?.[storeId]?.editor;
|
||||
const editor = useMemo(() => {
|
||||
if (storedEditor) {
|
||||
return storedEditor;
|
||||
@@ -60,8 +76,8 @@ export const BlockNoteContent = ({ doc, provider }: BlockNoteContentProps) => {
|
||||
}, [provider, storedEditor, userData?.email]);
|
||||
|
||||
useEffect(() => {
|
||||
setEditor(doc.id, editor);
|
||||
}, [setEditor, doc.id, editor]);
|
||||
setStore(storeId, { editor });
|
||||
}, [setStore, storeId, editor]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -77,7 +93,7 @@ export const BlockNoteContent = ({ doc, provider }: BlockNoteContentProps) => {
|
||||
<BlockNoteView
|
||||
editor={editor}
|
||||
formattingToolbar={false}
|
||||
editable={doc.abilities.partial_update}
|
||||
editable={doc.abilities.partial_update && !isVersion}
|
||||
theme="light"
|
||||
>
|
||||
<BlockNoteToolbar />
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { Alert, VariantType } from '@openfun/cunningham-react';
|
||||
import { Alert, Loader, VariantType } from '@openfun/cunningham-react';
|
||||
import { useRouter as useNavigate } from 'next/navigation';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Card } from '@/components';
|
||||
import { Box, Card, Text, TextErrors } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { DocHeader } from '@/features/docs/doc-header';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import {
|
||||
Panel,
|
||||
Versions,
|
||||
useDocVersion,
|
||||
} from '@/features/docs/doc-versioning/';
|
||||
|
||||
import { BlockNoteEditor } from './BlockNoteEditor';
|
||||
|
||||
@@ -12,24 +21,100 @@ interface DocEditorProps {
|
||||
}
|
||||
|
||||
export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||
const {
|
||||
query: { versionId },
|
||||
} = useRouter();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isVersion = versionId && typeof versionId === 'string';
|
||||
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocHeader doc={doc} />
|
||||
{!doc.abilities.partial_update && (
|
||||
<Box $margin={{ all: 'small', top: 'none' }}>
|
||||
<Alert
|
||||
type={VariantType.WARNING}
|
||||
>{`Read only, you don't have the right to update this document.`}</Alert>
|
||||
<Alert type={VariantType.WARNING}>
|
||||
{t(`Read only, you cannot edit this document.`)}
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
<Card
|
||||
$margin={{ all: 'big', top: 'none' }}
|
||||
$padding="big"
|
||||
$css="flex:1;"
|
||||
$overflow="auto"
|
||||
{isVersion && (
|
||||
<Box $margin={{ all: 'small', top: 'none' }}>
|
||||
<Alert type={VariantType.WARNING}>
|
||||
{t(`Read only, you cannot edit document versions.`)}
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
$background={colorsTokens()['primary-bg']}
|
||||
$height="100%"
|
||||
$direction="row"
|
||||
$margin={{ all: 'small', top: 'none' }}
|
||||
$gap="1rem"
|
||||
>
|
||||
<BlockNoteEditor doc={doc} />
|
||||
</Card>
|
||||
<Card $padding="big" $css="flex:1;" $overflow="auto">
|
||||
{isVersion ? (
|
||||
<DocVersionEditor doc={doc} versionId={versionId} />
|
||||
) : (
|
||||
<BlockNoteEditor doc={doc} />
|
||||
)}
|
||||
</Card>
|
||||
{doc.abilities.versions_list && <Panel doc={doc} />}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface DocVersionEditorProps {
|
||||
doc: Doc;
|
||||
versionId: Versions['version_id'];
|
||||
}
|
||||
|
||||
export const DocVersionEditor = ({ doc, versionId }: DocVersionEditorProps) => {
|
||||
const {
|
||||
data: version,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useDocVersion({
|
||||
docId: doc.id,
|
||||
versionId,
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (isError && error) {
|
||||
if (error.status === 404) {
|
||||
navigate.replace(`/404`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box $margin="large">
|
||||
<TextErrors
|
||||
causes={error.cause}
|
||||
icon={
|
||||
error.status === 502 ? (
|
||||
<Text className="material-icons" $theme="danger">
|
||||
wifi_off
|
||||
</Text>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading || !version) {
|
||||
return (
|
||||
<Box $align="center" $justify="center" $height="100%">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return <BlockNoteEditor doc={doc} version={version} />;
|
||||
};
|
||||
|
||||
@@ -5,12 +5,15 @@ import { useTranslation } from 'react-i18next';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { useUpdateDoc } from '@/features/docs/doc-management/';
|
||||
import { KEY_LIST_DOC_VERSIONS } from '@/features/docs/doc-versioning';
|
||||
|
||||
import { useDocStore } from '../stores';
|
||||
import { toBase64 } from '../utils';
|
||||
|
||||
const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
|
||||
const { toast } = useToastProvider();
|
||||
const { t } = useTranslation();
|
||||
const { forceSave, setForceSave } = useDocStore();
|
||||
|
||||
const { mutate: updateDoc } = useUpdateDoc({
|
||||
onSuccess: (data) => {
|
||||
@@ -19,8 +22,12 @@ const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
|
||||
docTitle: data.title,
|
||||
}),
|
||||
VariantType.SUCCESS,
|
||||
{
|
||||
duration: 1500,
|
||||
},
|
||||
);
|
||||
},
|
||||
listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
|
||||
});
|
||||
const [initialDoc, setInitialDoc] = useState<string>(
|
||||
toBase64(Y.encodeStateAsUpdate(doc)),
|
||||
@@ -57,10 +64,14 @@ const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
|
||||
/**
|
||||
* Check if the doc has been updated and can be saved.
|
||||
*/
|
||||
const shouldSave = useCallback(() => {
|
||||
const hasChanged = useCallback(() => {
|
||||
const newDoc = toBase64(Y.encodeStateAsUpdate(doc));
|
||||
return initialDoc !== newDoc && canSave;
|
||||
}, [canSave, doc, initialDoc]);
|
||||
return initialDoc !== newDoc;
|
||||
}, [doc, initialDoc]);
|
||||
|
||||
const shouldSave = useCallback(() => {
|
||||
return hasChanged() && canSave;
|
||||
}, [canSave, hasChanged]);
|
||||
|
||||
const saveDoc = useCallback(() => {
|
||||
const newDoc = toBase64(Y.encodeStateAsUpdate(doc));
|
||||
@@ -72,6 +83,18 @@ const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
|
||||
});
|
||||
}, [doc, docId, updateDoc]);
|
||||
|
||||
useEffect(() => {
|
||||
if (forceSave === 'false') {
|
||||
return;
|
||||
}
|
||||
|
||||
setForceSave('false');
|
||||
|
||||
if ((forceSave === 'current' && hasChanged()) || forceSave === 'version') {
|
||||
saveDoc();
|
||||
}
|
||||
}, [forceSave, hasChanged, saveDoc, setForceSave]);
|
||||
|
||||
const timeout = useRef<NodeJS.Timeout>();
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
@@ -4,60 +4,57 @@ import * as Y from 'yjs';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { signalingUrl } from '@/core';
|
||||
import { Base64, Doc } from '@/features/docs/doc-management';
|
||||
import { Base64 } from '@/features/docs/doc-management';
|
||||
|
||||
export interface DocStore {
|
||||
docsStore: {
|
||||
[docId: Doc['id']]: {
|
||||
provider: WebrtcProvider;
|
||||
editor?: BlockNoteEditor;
|
||||
};
|
||||
};
|
||||
createProvider: (docId: Doc['id'], initialDoc: Base64) => WebrtcProvider;
|
||||
setEditor: (docId: Doc['id'], editor: BlockNoteEditor) => void;
|
||||
interface DocStore {
|
||||
provider: WebrtcProvider;
|
||||
editor?: BlockNoteEditor;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
docsStore: {},
|
||||
};
|
||||
type ForceSaveState = 'false' | 'version' | 'current';
|
||||
|
||||
export const useDocStore = create<DocStore>((set) => ({
|
||||
docsStore: initialState.docsStore,
|
||||
createProvider: (docId: string, initialDoc: Base64) => {
|
||||
export interface UseDocStore {
|
||||
docsStore: {
|
||||
[storeId: string]: DocStore;
|
||||
};
|
||||
createProvider: (storeId: string, initialDoc: Base64) => WebrtcProvider;
|
||||
setStore: (storeId: string, props: Partial<DocStore>) => void;
|
||||
forceSave: ForceSaveState;
|
||||
setForceSave: (forceSave: ForceSaveState) => void;
|
||||
}
|
||||
|
||||
export const useDocStore = create<UseDocStore>((set, get) => ({
|
||||
docsStore: {},
|
||||
forceSave: 'false',
|
||||
setForceSave: (forceSave) => {
|
||||
set(() => ({ forceSave }));
|
||||
},
|
||||
createProvider: (storeId: string, initialDoc: Base64) => {
|
||||
const doc = new Y.Doc({
|
||||
guid: docId,
|
||||
guid: storeId,
|
||||
});
|
||||
|
||||
if (initialDoc) {
|
||||
Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64'));
|
||||
}
|
||||
|
||||
const provider = new WebrtcProvider(docId, doc, {
|
||||
signaling: [signalingUrl(docId)],
|
||||
const provider = new WebrtcProvider(storeId, doc, {
|
||||
signaling: [signalingUrl(storeId)],
|
||||
maxConns: 5,
|
||||
});
|
||||
|
||||
set(({ docsStore }) => {
|
||||
return {
|
||||
docsStore: {
|
||||
...docsStore,
|
||||
[docId]: {
|
||||
provider,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
get().setStore(storeId, { provider });
|
||||
|
||||
return provider;
|
||||
},
|
||||
setEditor: (docId, editor) => {
|
||||
setStore: (storeId, props) => {
|
||||
set(({ docsStore }) => {
|
||||
return {
|
||||
docsStore: {
|
||||
...docsStore,
|
||||
[docId]: {
|
||||
...docsStore[docId],
|
||||
editor,
|
||||
[storeId]: {
|
||||
...docsStore[storeId],
|
||||
...props,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
|
||||
|
||||
return (
|
||||
<Card
|
||||
$margin="big"
|
||||
$margin="small"
|
||||
aria-label={t('It is the card information about the document.')}
|
||||
>
|
||||
<Box $padding="small" $direction="row" $align="center">
|
||||
|
||||
@@ -104,7 +104,7 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
let body = await editor.blocksToHTMLLossy(editor.document);
|
||||
let body = await editor.blocksToFullHTML(editor.document);
|
||||
body = adaptBlockNoteHTML(body);
|
||||
|
||||
createPdf({
|
||||
|
||||
@@ -11,5 +11,15 @@ export function downloadFile(blob: Blob, filename: string) {
|
||||
}
|
||||
|
||||
export const adaptBlockNoteHTML = (html: string) => {
|
||||
return html.replaceAll('<p class="bn-inline-content"></p>', '<br/>');
|
||||
html = html.replaceAll('<p class="bn-inline-content"></p>', '<br/>');
|
||||
html = html.replaceAll(
|
||||
/data-text-alignment=\"([a-z]+)\"/g,
|
||||
'style="text-align: $1;"',
|
||||
);
|
||||
html = html.replaceAll(/data-text-color=\"([a-z]+)\"/g, 'style="color: $1;"');
|
||||
html = html.replaceAll(
|
||||
/data-background-color=\"([a-z]+)\"/g,
|
||||
'style="background-color: $1;"',
|
||||
);
|
||||
return html;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './useDocVersions';
|
||||
export * from './useDocVersion';
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, UseQueryOptionsAPI, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
import { Version } from '../types';
|
||||
|
||||
export type DocVersionParam = {
|
||||
docId: string;
|
||||
versionId: string;
|
||||
};
|
||||
|
||||
const getDocVersion = async ({
|
||||
versionId,
|
||||
docId,
|
||||
}: DocVersionParam): Promise<Version> => {
|
||||
const url = `documents/${docId}/versions/${versionId}/`;
|
||||
|
||||
const response = await fetchAPI(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
'Failed to get the doc version',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
|
||||
return response.json() as Promise<Version>;
|
||||
};
|
||||
|
||||
export const KEY_DOC_VERSION = 'doc-version';
|
||||
|
||||
export function useDocVersion(
|
||||
params: DocVersionParam,
|
||||
queryConfig?: UseQueryOptionsAPI<Version>,
|
||||
) {
|
||||
return useQuery<Version, APIError, Version>({
|
||||
queryKey: [KEY_DOC_VERSION, params],
|
||||
queryFn: () => getDocVersion(params),
|
||||
...queryConfig,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import {
|
||||
APIError,
|
||||
APIList,
|
||||
DefinedInitialDataInfiniteOptionsAPI,
|
||||
UseQueryOptionsAPI,
|
||||
errorCauses,
|
||||
fetchAPI,
|
||||
useAPIInfiniteQuery,
|
||||
} from '@/api';
|
||||
|
||||
import { Versions } from '../types';
|
||||
|
||||
export type DocVersionsParam = {
|
||||
docId: string;
|
||||
};
|
||||
|
||||
export type DocVersionsAPIParams = DocVersionsParam & {
|
||||
page: number;
|
||||
};
|
||||
|
||||
type VersionsResponse = APIList<Versions>;
|
||||
|
||||
const getDocVersions = async ({
|
||||
page,
|
||||
docId,
|
||||
}: DocVersionsAPIParams): Promise<VersionsResponse> => {
|
||||
const url = `documents/${docId}/versions/?page=${page}`;
|
||||
|
||||
const response = await fetchAPI(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
'Failed to get the doc versions',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
|
||||
return response.json() as Promise<VersionsResponse>;
|
||||
};
|
||||
|
||||
export const KEY_LIST_DOC_VERSIONS = 'doc-versions';
|
||||
|
||||
export function useDocVersions(
|
||||
params: DocVersionsAPIParams,
|
||||
queryConfig?: UseQueryOptionsAPI<VersionsResponse>,
|
||||
) {
|
||||
return useQuery<VersionsResponse, APIError, VersionsResponse>({
|
||||
queryKey: [KEY_LIST_DOC_VERSIONS, params],
|
||||
queryFn: () => getDocVersions(params),
|
||||
...queryConfig,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDocVersionsInfiniteQuery(
|
||||
param: DocVersionsParam,
|
||||
queryConfig?: DefinedInitialDataInfiniteOptionsAPI<VersionsResponse>,
|
||||
) {
|
||||
return useAPIInfiniteQuery<DocVersionsParam, VersionsResponse>(
|
||||
KEY_LIST_DOC_VERSIONS,
|
||||
getDocVersions,
|
||||
param,
|
||||
queryConfig,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Card, IconBG, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
|
||||
import { VersionList } from './VersionList';
|
||||
|
||||
interface PanelProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const Panel = ({ doc }: PanelProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const closedOverridingStyles = !isOpen && {
|
||||
$width: '0',
|
||||
$maxWidth: '0',
|
||||
$minWidth: '0',
|
||||
};
|
||||
|
||||
const transition = 'all 0.5s ease-in-out';
|
||||
|
||||
return (
|
||||
<Card
|
||||
$width="100%"
|
||||
$maxWidth="20rem"
|
||||
$position="sticky"
|
||||
$maxHeight="96vh"
|
||||
$height="100%"
|
||||
$css={`
|
||||
top: 2vh;
|
||||
transition: ${transition};
|
||||
${
|
||||
!isOpen &&
|
||||
`
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
`
|
||||
}
|
||||
`}
|
||||
aria-label={t('Document version panel')}
|
||||
{...closedOverridingStyles}
|
||||
>
|
||||
<IconBG
|
||||
iconName="menu_open"
|
||||
aria-label={
|
||||
isOpen
|
||||
? t('Close the document version panel')
|
||||
: t('Open the document version panel')
|
||||
}
|
||||
$background="transparent"
|
||||
$size="h2"
|
||||
$zIndex={1}
|
||||
$css={`
|
||||
cursor: pointer;
|
||||
left: ${isOpen ? '0' : '-3.3'}rem;
|
||||
top: 0.1rem;
|
||||
transform: rotate(${isOpen ? '180' : '0'}deg);
|
||||
transition: ${transition};
|
||||
user-select: none;
|
||||
`}
|
||||
$position="absolute"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
$radius="2px"
|
||||
/>
|
||||
<Box
|
||||
$overflow="hidden"
|
||||
$css={`
|
||||
opacity: ${isOpen ? '1' : '0'};
|
||||
transition: ${transition};
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
$padding={{ all: 'small' }}
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$justify="center"
|
||||
$css={`border-top: 2px solid ${colorsTokens()['primary-600']};`}
|
||||
>
|
||||
<Text $weight="bold" $size="l" $theme="primary">
|
||||
{t('VERSIONS')}
|
||||
</Text>
|
||||
</Box>
|
||||
<VersionList doc={doc} />
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { t } from 'i18next';
|
||||
import React, { PropsWithChildren, useState } from 'react';
|
||||
|
||||
import { Box, DropButton, IconOptions, StyledLink, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useDocStore } from '@/features/docs/doc-editor';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
|
||||
import { Versions } from '../types';
|
||||
import { revertUpdate } from '../utils';
|
||||
|
||||
interface VersionItemProps {
|
||||
docId: Doc['id'];
|
||||
text: string;
|
||||
link: string;
|
||||
versionId?: Versions['version_id'];
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export const VersionItem = ({
|
||||
docId,
|
||||
versionId,
|
||||
text,
|
||||
link,
|
||||
isActive,
|
||||
}: VersionItemProps) => {
|
||||
const { setForceSave, docsStore, setStore } = useDocStore();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const [isDropOpen, setIsDropOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="li"
|
||||
$background={isActive ? colorsTokens()['primary-300'] : 'transparent'}
|
||||
$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']};
|
||||
}
|
||||
`}
|
||||
$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">
|
||||
description
|
||||
</Text>
|
||||
<Text $weight="bold" $theme="primary" $size="m">
|
||||
{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={() => {
|
||||
setIsDropOpen(false);
|
||||
setForceSave(versionId ? 'version' : 'current');
|
||||
|
||||
if (
|
||||
!docsStore?.[docId]?.provider ||
|
||||
!docsStore?.[versionId]?.provider
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStore(docId, {
|
||||
editor: undefined,
|
||||
});
|
||||
|
||||
revertUpdate(
|
||||
docsStore[docId].provider.doc,
|
||||
docsStore[docId].provider.doc,
|
||||
docsStore[versionId].provider.doc,
|
||||
);
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">save</span>}
|
||||
size="small"
|
||||
>
|
||||
<Text $theme="primary">{t('Restore the version')}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</DropButton>
|
||||
)}
|
||||
</Box>
|
||||
</Link>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface LinkProps {
|
||||
href: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const Link = ({ href, children, isActive }: PropsWithChildren<LinkProps>) => {
|
||||
return isActive ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<StyledLink href={href}>{children}</StyledLink>
|
||||
);
|
||||
};
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { APIError } from '@/api';
|
||||
import { Box, InfiniteScroll, Text, TextErrors } from '@/components';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import { useDate } from '@/hook';
|
||||
|
||||
import { useDocVersionsInfiniteQuery } from '../api/useDocVersions';
|
||||
import { Versions } from '../types';
|
||||
|
||||
import { VersionItem } from './VersionItem';
|
||||
|
||||
interface VersionListStateProps {
|
||||
isLoading: boolean;
|
||||
error: APIError<unknown> | null;
|
||||
versions?: Versions[];
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
const VersionListState = ({
|
||||
isLoading,
|
||||
error,
|
||||
versions,
|
||||
doc,
|
||||
}: VersionListStateProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { formatDate } = useDate();
|
||||
const {
|
||||
query: { versionId },
|
||||
} = useRouter();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box $align="center" $margin="large">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<VersionItem
|
||||
text={t('Current version')}
|
||||
versionId={undefined}
|
||||
link={`/docs/${doc.id}/`}
|
||||
docId={doc.id}
|
||||
isActive={!versionId}
|
||||
/>
|
||||
{versions?.map((version) => (
|
||||
<VersionItem
|
||||
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}
|
||||
/>
|
||||
))}
|
||||
{error && (
|
||||
<Box
|
||||
$justify="center"
|
||||
$margin={{ vertical: 'small', horizontal: 'small' }}
|
||||
>
|
||||
<TextErrors
|
||||
causes={error.cause}
|
||||
icon={
|
||||
error.status === 502 ? (
|
||||
<Text $isMaterialIcon $theme="danger">
|
||||
wifi_off
|
||||
</Text>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface VersionListProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const VersionList = ({ doc }: VersionListProps) => {
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
isLoading,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useDocVersionsInfiniteQuery({
|
||||
docId: doc.id,
|
||||
});
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const versions = useMemo(() => {
|
||||
return data?.pages.reduce((acc, page) => {
|
||||
return acc.concat(page.results);
|
||||
}, [] as Versions[]);
|
||||
}, [data?.pages]);
|
||||
|
||||
return (
|
||||
<Box $css="overflow-y: auto; overflow-x: hidden;" ref={containerRef}>
|
||||
<InfiniteScroll
|
||||
hasMore={hasNextPage}
|
||||
isLoading={isFetchingNextPage}
|
||||
next={() => {
|
||||
void fetchNextPage();
|
||||
}}
|
||||
scrollContainer={containerRef.current}
|
||||
as="ul"
|
||||
$padding="none"
|
||||
$margin={{ top: 'none' }}
|
||||
role="listbox"
|
||||
>
|
||||
<VersionListState
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
versions={versions}
|
||||
doc={doc}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Panel';
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './api';
|
||||
export * from './components';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Doc } from '../doc-management';
|
||||
|
||||
export interface Versions {
|
||||
etag: string;
|
||||
is_latest: boolean;
|
||||
last_modified: string;
|
||||
version_id: string;
|
||||
}
|
||||
|
||||
export interface Version {
|
||||
content: Doc['content'];
|
||||
last_modified: string;
|
||||
id: string;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as Y from 'yjs';
|
||||
|
||||
/**
|
||||
* Revert the doc to a previous state.
|
||||
*
|
||||
* We cannot simply replace a doc with another previous doc,
|
||||
* because Y.js will act as if the previous doc is a new doc and so
|
||||
* merge it with the current doc, so we need to revert the doc (undo).
|
||||
*
|
||||
* To do so we simulate a history of the doc by saving snapshots of the doc
|
||||
* and then revert the doc to a previous snapshot.
|
||||
*
|
||||
* @param doc
|
||||
* @param snapshotOrigin
|
||||
* @param snapshotUpdate
|
||||
*/
|
||||
export function revertUpdate(
|
||||
doc: Y.Doc,
|
||||
snapshotOrigin: Y.Doc,
|
||||
snapshotUpdate: Y.Doc,
|
||||
) {
|
||||
try {
|
||||
const snapshotDoc = new Y.Doc();
|
||||
Y.applyUpdate(
|
||||
snapshotDoc,
|
||||
Y.encodeStateAsUpdate(snapshotUpdate),
|
||||
snapshotOrigin,
|
||||
);
|
||||
|
||||
const currentStateVector = Y.encodeStateVector(doc);
|
||||
const snapshotStateVector = Y.encodeStateVector(snapshotDoc);
|
||||
|
||||
const changesSinceSnapshotUpdate = Y.encodeStateAsUpdate(
|
||||
doc,
|
||||
snapshotStateVector,
|
||||
);
|
||||
|
||||
const undoManager = new Y.UndoManager(
|
||||
[snapshotDoc.getMap('document-store')],
|
||||
{
|
||||
trackedOrigins: new Set([snapshotOrigin]),
|
||||
},
|
||||
);
|
||||
|
||||
Y.applyUpdate(snapshotDoc, changesSinceSnapshotUpdate, snapshotOrigin);
|
||||
undoManager.undo();
|
||||
const revertChangesSinceSnapshotUpdate = Y.encodeStateAsUpdate(
|
||||
snapshotDoc,
|
||||
currentStateVector,
|
||||
);
|
||||
Y.applyUpdate(doc, revertChangesSinceSnapshotUpdate, snapshotOrigin);
|
||||
} catch (e) {}
|
||||
}
|
||||
@@ -20,9 +20,6 @@ import { PAGE_SIZE } from '../conf';
|
||||
import { DocsGridActions } from './DocsGridActions';
|
||||
|
||||
const DocsGridStyle = createGlobalStyle`
|
||||
& .c__datagrid{
|
||||
max-height: 91%;
|
||||
}
|
||||
& .c__datagrid thead{
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@@ -171,7 +168,7 @@ export const DocsGrid = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Users number'),
|
||||
headerName: t('Members'),
|
||||
id: 'users_number',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
@@ -193,6 +190,7 @@ export const DocsGrid = () => {
|
||||
pagination={pagination}
|
||||
onSortModelChange={setSortModel}
|
||||
sortModel={sortModel}
|
||||
emptyPlaceholderLabel={t("You don't have any document yet.")}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
+16
-6
@@ -70,14 +70,23 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
|
||||
duration: 4000,
|
||||
};
|
||||
|
||||
const onError = (dataError: APIErrorUser['data']) => {
|
||||
const messageError =
|
||||
dataError?.type === OptionType.INVITATION
|
||||
const onError = (dataError: APIErrorUser) => {
|
||||
let messageError =
|
||||
dataError['data']?.type === OptionType.INVITATION
|
||||
? t(`Failed to create the invitation for {{email}}.`, {
|
||||
email: dataError?.value,
|
||||
email: dataError['data']?.value,
|
||||
})
|
||||
: t(`Failed to add the member in the document.`);
|
||||
|
||||
if (
|
||||
dataError.cause?.[0] ===
|
||||
'Document invitation with this Email address and Document already exists.'
|
||||
) {
|
||||
messageError = t('"{{email}}" is already invited to the document.', {
|
||||
email: dataError['data']?.value,
|
||||
});
|
||||
}
|
||||
|
||||
toast(messageError, VariantType.ERROR, toastOptions);
|
||||
};
|
||||
|
||||
@@ -106,11 +115,12 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
|
||||
|
||||
setIsPending(false);
|
||||
setResetKey(resetKey + 1);
|
||||
setSelectedUsers([]);
|
||||
|
||||
settledPromises.forEach((settledPromise) => {
|
||||
switch (settledPromise.status) {
|
||||
case 'rejected':
|
||||
onError((settledPromise.reason as APIErrorUser).data);
|
||||
onError(settledPromise.reason as APIErrorUser);
|
||||
break;
|
||||
|
||||
case 'fulfilled':
|
||||
@@ -132,7 +142,7 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
|
||||
<IconBG iconName="group_add" />
|
||||
<Box $gap="0.7rem" $direction="row" $wrap="wrap" $css="flex: 70%;">
|
||||
<Box $gap="0.7rem" $direction="row" $wrap="wrap" $css="flex: 80%;">
|
||||
<Box $css="flex: auto;">
|
||||
<Box $css="flex: auto;" $width="15rem">
|
||||
<SearchUsers
|
||||
key={resetKey + 1}
|
||||
doc={doc}
|
||||
|
||||
+26
-14
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Options } from 'react-select';
|
||||
import { InputActionMeta, Options } from 'react-select';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
@@ -42,7 +42,7 @@ export const SearchUsers = ({
|
||||
|
||||
const options = data?.results;
|
||||
|
||||
useEffect(() => {
|
||||
const optionsSelect = useMemo(() => {
|
||||
if (!resolveOptionsRef.current || !options) {
|
||||
return;
|
||||
}
|
||||
@@ -81,6 +81,8 @@ export const SearchUsers = ({
|
||||
|
||||
resolveOptionsRef.current(users);
|
||||
resolveOptionsRef.current = null;
|
||||
|
||||
return users;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [options, selectedUsers]);
|
||||
|
||||
@@ -91,16 +93,26 @@ export const SearchUsers = ({
|
||||
};
|
||||
|
||||
const timeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const onInputChangeHandle = useCallback((newValue: string) => {
|
||||
setInput(newValue);
|
||||
if (timeout.current) {
|
||||
clearTimeout(timeout.current);
|
||||
}
|
||||
const onInputChangeHandle = useCallback(
|
||||
(newValue: string, actionMeta: InputActionMeta) => {
|
||||
if (
|
||||
actionMeta.action === 'input-blur' ||
|
||||
actionMeta.action === 'menu-close'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
timeout.current = setTimeout(() => {
|
||||
setUserQuery(newValue);
|
||||
}, 1000);
|
||||
}, []);
|
||||
setInput(newValue);
|
||||
if (timeout.current) {
|
||||
clearTimeout(timeout.current);
|
||||
}
|
||||
|
||||
timeout.current = setTimeout(() => {
|
||||
setUserQuery(newValue);
|
||||
}, 1000);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<AsyncSelect
|
||||
@@ -125,10 +137,10 @@ export const SearchUsers = ({
|
||||
aria-label={t('Find a member to add to the document')}
|
||||
isMulti
|
||||
loadOptions={loadOptions}
|
||||
defaultOptions={[]}
|
||||
defaultOptions={optionsSelect}
|
||||
onInputChange={onInputChangeHandle}
|
||||
inputValue={input}
|
||||
placeholder={t('Search new members by email')}
|
||||
placeholder={t('Search by email')}
|
||||
noOptionsMessage={() =>
|
||||
input
|
||||
? t("We didn't find a mail matching, try to be more accurate")
|
||||
|
||||
+9
-1
@@ -5,7 +5,12 @@ import {
|
||||
} from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { Access, KEY_DOC, Role } from '@/features/docs/doc-management';
|
||||
import {
|
||||
Access,
|
||||
KEY_DOC,
|
||||
KEY_LIST_DOC,
|
||||
Role,
|
||||
} from '@/features/docs/doc-management';
|
||||
|
||||
import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses';
|
||||
|
||||
@@ -54,6 +59,9 @@ export const useUpdateDocAccess = (options?: UseUpdateDocAccessOptions) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_DOC],
|
||||
});
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_LIST_DOC],
|
||||
});
|
||||
if (options?.onSuccess) {
|
||||
options.onSuccess(data, variables, context);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,13 @@ interface OptionsMutate {
|
||||
type: 'update' | 'delete' | 'create';
|
||||
}
|
||||
|
||||
type Options = (OptionsReadonly | OptionsMutate) & { syncManager: SyncManager };
|
||||
interface OptionsSync {
|
||||
type: 'synch';
|
||||
}
|
||||
|
||||
type Options = (OptionsReadonly | OptionsMutate | OptionsSync) & {
|
||||
syncManager: SyncManager;
|
||||
};
|
||||
|
||||
export class ApiPlugin implements WorkboxPlugin {
|
||||
private readonly options: Options;
|
||||
|
||||
@@ -28,7 +28,8 @@ export const isApiUrl = (href: string) => {
|
||||
*/
|
||||
registerRoute(
|
||||
({ url }) =>
|
||||
isApiUrl(url.href) && url.href.match(/.*\/documents\/\?(page|ordering)=.*/),
|
||||
isApiUrl(url.href) &&
|
||||
url.href.match(/.*\/documents\/\?(page|ordering)=.*/g),
|
||||
new NetworkOnly({
|
||||
plugins: [
|
||||
new ApiPlugin({
|
||||
@@ -42,7 +43,8 @@ registerRoute(
|
||||
);
|
||||
|
||||
registerRoute(
|
||||
({ url }) => isApiUrl(url.href) && url.href.match(/.*\/documents\/.*\//),
|
||||
({ url }) =>
|
||||
isApiUrl(url.href) && url.href.match(/.*\/documents\/([a-z0-9\-]+)\/$/g),
|
||||
new NetworkOnly({
|
||||
plugins: [
|
||||
new ApiPlugin({
|
||||
@@ -56,7 +58,8 @@ registerRoute(
|
||||
);
|
||||
|
||||
registerRoute(
|
||||
({ url }) => isApiUrl(url.href) && url.href.match(/.*\/documents\/.*\//),
|
||||
({ url }) =>
|
||||
isApiUrl(url.href) && url.href.match(/.*\/documents\/([a-z0-9\-]+)\/$/g),
|
||||
new NetworkOnly({
|
||||
plugins: [
|
||||
new ApiPlugin({
|
||||
@@ -69,7 +72,7 @@ registerRoute(
|
||||
);
|
||||
|
||||
registerRoute(
|
||||
({ url }) => isApiUrl(url.href) && url.href.match(/.*\/documents\//),
|
||||
({ url }) => isApiUrl(url.href) && url.href.match(/.*\/documents\/$/g),
|
||||
new NetworkOnly({
|
||||
plugins: [
|
||||
new ApiPlugin({
|
||||
@@ -82,7 +85,8 @@ registerRoute(
|
||||
);
|
||||
|
||||
registerRoute(
|
||||
({ url }) => isApiUrl(url.href) && url.href.match(/.*\/documents\/.*\//),
|
||||
({ url }) =>
|
||||
isApiUrl(url.href) && url.href.match(/.*\/documents\/([a-z0-9\-]+)\/$/g),
|
||||
new NetworkOnly({
|
||||
plugins: [
|
||||
new ApiPlugin({
|
||||
@@ -103,6 +107,10 @@ registerRoute(
|
||||
new ExpirationPlugin({
|
||||
maxAgeSeconds: 24 * 60 * 60 * DAYS_EXP,
|
||||
}),
|
||||
new ApiPlugin({
|
||||
type: 'synch',
|
||||
syncManager,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
'GET',
|
||||
|
||||
@@ -87,6 +87,7 @@ 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 = [
|
||||
@@ -99,6 +100,7 @@ const precacheResources = [
|
||||
FALLBACK.offline,
|
||||
FALLBACK.images,
|
||||
FALLBACK.docs,
|
||||
FALLBACK.versions,
|
||||
];
|
||||
|
||||
const precacheStrategy = getStrategy({
|
||||
@@ -117,8 +119,14 @@ setCatchHandler(async ({ request, url, event }) => {
|
||||
return ApiPlugin.getApiCatchHandler();
|
||||
|
||||
case request.destination === 'document':
|
||||
if (url.pathname.match(/^\/docs\/.*\//)) {
|
||||
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 });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DateTime, DateTimeFormatOptions } from 'luxon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const format: DateTimeFormatOptions = {
|
||||
const formatDefault: DateTimeFormatOptions = {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
year: 'numeric',
|
||||
@@ -12,7 +12,10 @@ const format: DateTimeFormatOptions = {
|
||||
export const useDate = () => {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const formatDate = (date: string): string => {
|
||||
const formatDate = (
|
||||
date: string,
|
||||
format: DateTimeFormatOptions = formatDefault,
|
||||
): string => {
|
||||
return DateTime.fromISO(date)
|
||||
.setLocale(i18n.language)
|
||||
.toLocaleString(format);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"en": { "translation": {} },
|
||||
"fr": {
|
||||
"translation": {
|
||||
"\"{{email}}\" is already invited to the document.": "\"{{email}}\" est déjà invité à accéder au document.",
|
||||
"Accessibility": "Accessibilité",
|
||||
"Accessibility statement": "Déclaration d'accessibilité",
|
||||
"Address:": "Adresse :",
|
||||
@@ -11,6 +12,7 @@
|
||||
"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 document version panel": "Fermer le panneau des versions du document",
|
||||
"Close the modal": "Fermer la modale",
|
||||
"Compliance status": "État de conformité",
|
||||
"Confirm deletion": "Confirmer la suppression",
|
||||
@@ -21,6 +23,7 @@
|
||||
"Create a new document": "Créer un nouveau document",
|
||||
"Create the document": "Créer le 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",
|
||||
@@ -30,6 +33,7 @@
|
||||
"Docs Logo": "Logo Docs",
|
||||
"Document icon": "Icône du document",
|
||||
"Document name": "Nom du document",
|
||||
"Document version panel": "Panneau des versions du document",
|
||||
"Documents": "Documents",
|
||||
"Download": "Télécharger",
|
||||
"E-mail:": "E-mail:",
|
||||
@@ -59,12 +63,15 @@
|
||||
"List members card": "Carte liste des membres",
|
||||
"Logout": "Se déconnecter",
|
||||
"Marianne Logo": "Logo Marianne",
|
||||
"Members": "Membres",
|
||||
"More info?": "Plus d'infos ?",
|
||||
"My account": "Mon compte",
|
||||
"No editor found": "Pas d'éditeur trouvé",
|
||||
"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 ?!",
|
||||
"Open the document options": "Ouvrir les options du document",
|
||||
"Open the document version panel": "Ouvrir le panneau des versions du document",
|
||||
"Open the version options": "Ouvrir les options de version",
|
||||
"Ouch !": "Aïe !",
|
||||
"Owner": "Propriétaire",
|
||||
"Owners:": "Propriétaires:",
|
||||
@@ -72,10 +79,13 @@
|
||||
"Public": "Public",
|
||||
"Publication Director": "Directeur de la publication",
|
||||
"Publisher": "Éditeur",
|
||||
"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",
|
||||
"Restore the version": "Restaurer la version",
|
||||
"Role": "Rôle",
|
||||
"Search new members by email": "Rechercher les nouveaux membres par e-mail",
|
||||
"Search by email": "Recherche par email",
|
||||
"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",
|
||||
"Something bad happens, please retry.": "Une erreur inattendue s'est produite, veuillez réessayer.",
|
||||
@@ -97,7 +107,7 @@
|
||||
"Update document \"{{documentTitle}}\"": "Mettre à jour le document \"{{documentTitle}}\"",
|
||||
"Updated at": "Mise à jour le",
|
||||
"User {{email}} added to the document.": "L'utilisateur {{email}} a été ajouté au document.",
|
||||
"Users number": "Nombre d'utilisateurs",
|
||||
"VERSIONS": "VERSIONS",
|
||||
"Validate": "Valider",
|
||||
"Validate the modification": "Valider les modifications",
|
||||
"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",
|
||||
@@ -107,6 +117,7 @@
|
||||
"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 document \"{{docTitle}}\" has been saved.": "Votre document \"{{docTitle}}\" a été enregistré.",
|
||||
"Your pdf was downloaded succesfully": "Votre pdf a été téléchargé avec succès",
|
||||
"Your role": "Votre rôle",
|
||||
|
||||
@@ -8,12 +8,12 @@ export function MainLayout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box $height="100vh">
|
||||
<Box $minHeight="100vh">
|
||||
<Header />
|
||||
<Box $css="flex: 1;" $direction="row">
|
||||
<Box
|
||||
as="main"
|
||||
$height={`calc(100vh - ${HEADER_HEIGHT})`}
|
||||
$minHeight={`calc(100vh - ${HEADER_HEIGHT})`}
|
||||
$width="100%"
|
||||
$background={colorsTokens()['primary-bg']}
|
||||
>
|
||||
|
||||
+18
-10
@@ -1,15 +1,15 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { useRouter as useNavigate } from 'next/navigation';
|
||||
import { useRouter } from 'next/router';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
import { Box, Text, TextErrors } from '@/components/';
|
||||
import { DocEditor } from '@/features/docs/doc-editor';
|
||||
import { Box, Text } from '@/components';
|
||||
import { TextErrors } from '@/components/TextErrors';
|
||||
import { DocEditor } from '@/features/docs';
|
||||
import { useDoc } from '@/features/docs/doc-management';
|
||||
import { MainLayout } from '@/layouts';
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
export function DocLayout() {
|
||||
const {
|
||||
query: { id },
|
||||
} = useRouter();
|
||||
@@ -18,14 +18,18 @@ const Page: NextPageWithLayout = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Doc id={id} />;
|
||||
};
|
||||
return (
|
||||
<MainLayout>
|
||||
<DocPage id={id} />
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
|
||||
interface DocProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const Doc = ({ id }: DocProps) => {
|
||||
const DocPage = ({ id }: DocProps) => {
|
||||
const { data: doc, isLoading, isError, error } = useDoc({ id });
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -41,7 +45,7 @@ const Doc = ({ id }: DocProps) => {
|
||||
causes={error.cause}
|
||||
icon={
|
||||
error.status === 502 ? (
|
||||
<Text className="material-icons" $theme="danger">
|
||||
<Text $isMaterialIcon $theme="danger">
|
||||
wifi_off
|
||||
</Text>
|
||||
) : undefined
|
||||
@@ -62,8 +66,12 @@ const Doc = ({ id }: DocProps) => {
|
||||
return <DocEditor doc={doc} />;
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
return <MainLayout>{page}</MainLayout>;
|
||||
const Page: NextPageWithLayout = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout() {
|
||||
return <DocLayout />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
import { DocLayout } from '../index';
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout() {
|
||||
return <DocLayout />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
import { DocLayout } from '../index';
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout() {
|
||||
return <DocLayout />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -31,3 +31,13 @@ main ::-webkit-scrollbar-thumb:hover,
|
||||
.ReactModalPortal ::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #a8bbbf;
|
||||
}
|
||||
|
||||
.btn-unstyled {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"watch": ["src"],
|
||||
"ext": "ts",
|
||||
"exec": "ts-node --esm ./src/server.ts"
|
||||
"exec": "yarn build"
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "y-webrtc-signaling",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.0",
|
||||
"description": "WebRTC server for Yjs",
|
||||
"repository": "https://github.com/numerique-gouv/impress",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc -p .",
|
||||
"dev": "nodemon --exec src/server.ts",
|
||||
"dev": "nodemon --config nodemon.json",
|
||||
"start": "node ./dist/server.js",
|
||||
"lint": "eslint . --ext .ts"
|
||||
},
|
||||
@@ -15,15 +15,15 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"lib0": "0.2.94",
|
||||
"ws": "8.17.1"
|
||||
"lib0": "0.2.95",
|
||||
"ws": "8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/ws": "8.5.10",
|
||||
"@types/ws": "8.5.12",
|
||||
"eslint-config-impress": "*",
|
||||
"nodemon": "3.1.4",
|
||||
"ts-jest": "29.1.5",
|
||||
"ts-jest": "29.2.3",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "*"
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ const onconnection = (conn: WebSocket, url: string) => {
|
||||
subscribedTopics.forEach((topicName) => {
|
||||
const subs = topics.get(topicName) || new Set();
|
||||
subs.forEach((sub) => {
|
||||
if (sub.conn === conn) {
|
||||
if (sub.url === url && sub.conn === conn) {
|
||||
subs.delete(sub);
|
||||
}
|
||||
});
|
||||
@@ -96,9 +96,18 @@ const onconnection = (conn: WebSocket, url: string) => {
|
||||
topicName,
|
||||
() => new Set(),
|
||||
);
|
||||
topic.add({ url, conn });
|
||||
// add topic to conn
|
||||
subscribedTopics.add(topicName);
|
||||
|
||||
let isAlreadyAdded = false;
|
||||
topic.forEach((sub) => {
|
||||
if (sub.url === url && sub.conn === conn) {
|
||||
isAlreadyAdded = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!isAlreadyAdded) {
|
||||
topic.add({ url, conn });
|
||||
subscribedTopics.add(topicName);
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impress",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
@@ -24,15 +24,15 @@
|
||||
"i18n:test": "yarn I18N run test"
|
||||
},
|
||||
"resolutions": {
|
||||
"@blocknote/core": "0.14.2",
|
||||
"@blocknote/mantine": "0.14.2",
|
||||
"@blocknote/react": "0.14.2",
|
||||
"@types/node": "20.14.9",
|
||||
"@blocknote/core": "0.15.3",
|
||||
"@blocknote/mantine": "0.15.3",
|
||||
"@blocknote/react": "0.15.3",
|
||||
"@types/node": "20.14.13",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"cross-env": "7.0.3",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"typescript": "5.5.2",
|
||||
"typescript": "5.5.4",
|
||||
"yjs": "13.6.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"name": "eslint-config-impress",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "14.2.4",
|
||||
"@tanstack/eslint-plugin-query": "5.47.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@next/eslint-plugin-next": "14.2.5",
|
||||
"@tanstack/eslint-plugin-query": "5.51.15",
|
||||
"@typescript-eslint/eslint-plugin": "7.13.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-next": "14.2.4",
|
||||
"eslint-config-next": "14.2.5",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-jest": "28.6.0",
|
||||
"eslint-plugin-jsx-a11y": "6.9.0",
|
||||
"eslint-plugin-playwright": "1.6.2",
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"eslint-plugin-react": "7.34.3",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"eslint-plugin-react": "7.35.0",
|
||||
"eslint-plugin-testing-library": "6.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "packages-i18n",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"extract-translation": "yarn extract-translation:impress",
|
||||
@@ -17,7 +17,7 @@
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"i18next-parser": "8.8.0",
|
||||
"jest": "29.7.0",
|
||||
"ts-jest": "29.1.5",
|
||||
"ts-jest": "29.2.3",
|
||||
"typescript": "*",
|
||||
"yargs": "17.7.2"
|
||||
}
|
||||
|
||||
+1742
-461
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
image:
|
||||
repository: lasuite/impress-backend
|
||||
pullPolicy: Always
|
||||
tag: "v1.0.0"
|
||||
tag: "v1.2.0-preprod"
|
||||
|
||||
backend:
|
||||
migrateJobAnnotations:
|
||||
@@ -115,13 +115,13 @@ frontend:
|
||||
image:
|
||||
repository: lasuite/impress-frontend
|
||||
pullPolicy: Always
|
||||
tag: "v1.0.0"
|
||||
tag: "v1.2.0-preprod"
|
||||
|
||||
webrtc:
|
||||
image:
|
||||
repository: lasuite/impress-y-webrtc-signaling
|
||||
pullPolicy: Always
|
||||
tag: "v1.0.0"
|
||||
tag: "v1.2.0-preprod"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
image:
|
||||
repository: lasuite/impress-backend
|
||||
pullPolicy: Always
|
||||
tag: "v1.0.0"
|
||||
tag: "v1.2.0"
|
||||
|
||||
backend:
|
||||
migrateJobAnnotations:
|
||||
@@ -115,13 +115,13 @@ frontend:
|
||||
image:
|
||||
repository: lasuite/impress-frontend
|
||||
pullPolicy: Always
|
||||
tag: "v1.0.0"
|
||||
tag: "v1.2.0"
|
||||
|
||||
webrtc:
|
||||
image:
|
||||
repository: lasuite/impress-y-webrtc-signaling
|
||||
pullPolicy: Always
|
||||
tag: "v1.0.0"
|
||||
tag: "v1.2.0"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.0",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impress-openapi-client-ts",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"description": "Tool to generate Typescript API client for the impress application.",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user