Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cbd43caae |
@@ -15,13 +15,10 @@ and this project adheres to
|
|||||||
- 💄(frontend) improve comments highlights #1961
|
- 💄(frontend) improve comments highlights #1961
|
||||||
- ♿️(frontend) improve BoxButton a11y and native button semantics #2103
|
- ♿️(frontend) improve BoxButton a11y and native button semantics #2103
|
||||||
- ♿️(frontend) improve language picker accessibility #2069
|
- ♿️(frontend) improve language picker accessibility #2069
|
||||||
- ♿️(frontend) add aria-hidden to decorative icons in dropdown menu #2093
|
|
||||||
- 🐛(backend) move lock table closer to the insert operation targeted
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- 🐛(y-provider) destroy Y.Doc instances after each convert request #2129
|
- 🐛(y-provider) destroy Y.Doc instances after each convert request #2129
|
||||||
- 🐛(backend) remove deleted sub documents in favorite_list endpoint #2083
|
|
||||||
|
|
||||||
## [v4.8.3] - 2026-03-23
|
## [v4.8.3] - 2026-03-23
|
||||||
|
|
||||||
|
|||||||
@@ -674,8 +674,17 @@ class DocumentViewSet(
|
|||||||
|
|
||||||
return drf.response.Response(serializer.data)
|
return drf.response.Response(serializer.data)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
"""Set the current user as creator and owner of the newly created object."""
|
"""Set the current user as creator and owner of the newly created object."""
|
||||||
|
|
||||||
|
# locks the table to ensure safe concurrent access
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001
|
||||||
|
"IN SHARE ROW EXCLUSIVE MODE;"
|
||||||
|
)
|
||||||
|
|
||||||
# Remove file from validated_data as it's not a model field
|
# Remove file from validated_data as it's not a model field
|
||||||
# Process it if present
|
# Process it if present
|
||||||
uploaded_file = serializer.validated_data.pop("file", None)
|
uploaded_file = serializer.validated_data.pop("file", None)
|
||||||
@@ -693,25 +702,15 @@ class DocumentViewSet(
|
|||||||
)
|
)
|
||||||
serializer.validated_data["content"] = converted_content
|
serializer.validated_data["content"] = converted_content
|
||||||
serializer.validated_data["title"] = uploaded_file.name
|
serializer.validated_data["title"] = uploaded_file.name
|
||||||
logger.info("conversion ended successfully")
|
|
||||||
except ConversionError as err:
|
except ConversionError as err:
|
||||||
logger.error("could not convert file content with error: %s", err)
|
|
||||||
raise drf.exceptions.ValidationError(
|
raise drf.exceptions.ValidationError(
|
||||||
{"file": ["Could not convert file content"]}
|
{"file": ["Could not convert file content"]}
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
with transaction.atomic():
|
obj = models.Document.add_root(
|
||||||
# locks the table to ensure safe concurrent access
|
creator=self.request.user,
|
||||||
with connection.cursor() as cursor:
|
**serializer.validated_data,
|
||||||
cursor.execute(
|
)
|
||||||
f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001
|
|
||||||
"IN SHARE ROW EXCLUSIVE MODE;"
|
|
||||||
)
|
|
||||||
|
|
||||||
obj = models.Document.add_root(
|
|
||||||
creator=self.request.user,
|
|
||||||
**serializer.validated_data,
|
|
||||||
)
|
|
||||||
serializer.instance = obj
|
serializer.instance = obj
|
||||||
models.DocumentAccess.objects.create(
|
models.DocumentAccess.objects.create(
|
||||||
document=obj,
|
document=obj,
|
||||||
@@ -828,7 +827,6 @@ class DocumentViewSet(
|
|||||||
|
|
||||||
queryset = self.queryset.filter(path_list)
|
queryset = self.queryset.filter(path_list)
|
||||||
queryset = queryset.filter(id__in=favorite_documents_ids)
|
queryset = queryset.filter(id__in=favorite_documents_ids)
|
||||||
queryset = queryset.filter(ancestors_deleted_at__isnull=True)
|
|
||||||
queryset = queryset.annotate_user_roles(user)
|
queryset = queryset.annotate_user_roles(user)
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
is_favorite=db.Value(True, output_field=db.BooleanField())
|
is_favorite=db.Value(True, output_field=db.BooleanField())
|
||||||
|
|||||||
+11
-10
@@ -267,16 +267,6 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
|||||||
if settings.USER_ONBOARDING_SANDBOX_DOCUMENT:
|
if settings.USER_ONBOARDING_SANDBOX_DOCUMENT:
|
||||||
# transaction.atomic is used in a context manager to avoid a transaction if
|
# transaction.atomic is used in a context manager to avoid a transaction if
|
||||||
# the settings USER_ONBOARDING_SANDBOX_DOCUMENT is unused
|
# the settings USER_ONBOARDING_SANDBOX_DOCUMENT is unused
|
||||||
sandbox_id = settings.USER_ONBOARDING_SANDBOX_DOCUMENT
|
|
||||||
try:
|
|
||||||
template_document = Document.objects.get(id=sandbox_id)
|
|
||||||
except Document.DoesNotExist:
|
|
||||||
logger.warning(
|
|
||||||
"Onboarding sandbox document with id %s does not exist. Skipping.",
|
|
||||||
sandbox_id,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# locks the table to ensure safe concurrent access
|
# locks the table to ensure safe concurrent access
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
@@ -284,6 +274,17 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
|||||||
f'LOCK TABLE "{Document._meta.db_table}" ' # noqa: SLF001
|
f'LOCK TABLE "{Document._meta.db_table}" ' # noqa: SLF001
|
||||||
"IN SHARE ROW EXCLUSIVE MODE;"
|
"IN SHARE ROW EXCLUSIVE MODE;"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sandbox_id = settings.USER_ONBOARDING_SANDBOX_DOCUMENT
|
||||||
|
try:
|
||||||
|
template_document = Document.objects.get(id=sandbox_id)
|
||||||
|
except Document.DoesNotExist:
|
||||||
|
logger.warning(
|
||||||
|
"Onboarding sandbox document with id %s does not exist. Skipping.",
|
||||||
|
sandbox_id,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
sandbox_document = Document.add_root(
|
sandbox_document = Document.add_root(
|
||||||
title=template_document.title,
|
title=template_document.title,
|
||||||
content=template_document.content,
|
content=template_document.content,
|
||||||
|
|||||||
@@ -45,8 +45,6 @@ class Converter:
|
|||||||
def convert(self, data, content_type, accept):
|
def convert(self, data, content_type, accept):
|
||||||
"""Convert input into other formats using external microservices."""
|
"""Convert input into other formats using external microservices."""
|
||||||
|
|
||||||
logger.info("converting content from %s to %s", content_type, accept)
|
|
||||||
|
|
||||||
if content_type == mime_types.DOCX and accept == mime_types.YJS:
|
if content_type == mime_types.DOCX and accept == mime_types.YJS:
|
||||||
blocknote_data = self.docspec.convert(
|
blocknote_data = self.docspec.convert(
|
||||||
data, mime_types.DOCX, mime_types.BLOCKNOTE
|
data, mime_types.DOCX, mime_types.BLOCKNOTE
|
||||||
|
|||||||
@@ -114,29 +114,3 @@ def test_api_document_favorite_list_with_favorite_children():
|
|||||||
assert content[0]["id"] == str(children[0].id)
|
assert content[0]["id"] == str(children[0].id)
|
||||||
assert content[1]["id"] == str(children[1].id)
|
assert content[1]["id"] == str(children[1].id)
|
||||||
assert content[2]["id"] == str(access.document.id)
|
assert content[2]["id"] == str(access.document.id)
|
||||||
|
|
||||||
|
|
||||||
def test_api_document_favorite_list_with_deleted_child():
|
|
||||||
"""
|
|
||||||
Authenticated users should not see deleted documents in their favorite list.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory()
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
root = factories.DocumentFactory(creator=user, users=[user], favorited_by=[user])
|
|
||||||
child1, child2 = factories.DocumentFactory.create_batch(
|
|
||||||
2, parent=root, favorited_by=[user]
|
|
||||||
)
|
|
||||||
|
|
||||||
child1.delete()
|
|
||||||
|
|
||||||
response = client.get("/api/v1.0/documents/favorite_list/")
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json()["count"] == 2
|
|
||||||
|
|
||||||
content = response.json()["results"]
|
|
||||||
|
|
||||||
assert content[0]["id"] == str(root.id)
|
|
||||||
assert content[1]["id"] == str(child2.id)
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
PORT=3000
|
|
||||||
BASE_URL=http://localhost:3000
|
|
||||||
BASE_API_URL=http://localhost:8071/api/v1.0
|
|
||||||
CUSTOM_SIGN_IN=false
|
|
||||||
TEST_INSTANCE_ONLY=false
|
|
||||||
SIGN_IN_EL_TRIGGER=Start Writing
|
|
||||||
FIRST_NAME=E2E
|
|
||||||
SIGN_IN_USERNAME_CHROMIUM=user.test@chromium.test
|
|
||||||
SIGN_IN_USERNAME_WEBKIT=user.test@webkit.com
|
|
||||||
SIGN_IN_USERNAME_FIREFOX=user.test@firefox.com
|
|
||||||
# To test server to server API calls
|
|
||||||
SERVER_TO_SERVER_API_TOKENS='server-api-token'
|
|
||||||
SUB_CHROMIUM=user.test@chromium.test
|
|
||||||
SUB_WEBKIT=user.test@webkit.com
|
|
||||||
SUB_FIREFOX=user.test@firefox.com
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
PORT=3000
|
|
||||||
BASE_URL=http://localhost:3000
|
|
||||||
BASE_API_URL=http://localhost:8071/api/v1.0
|
|
||||||
TEST_INSTANCE_ONLY=false
|
|
||||||
CUSTOM_SIGN_IN=false
|
|
||||||
SIGN_IN_EL_TRIGGER=Start Writing
|
|
||||||
FIRST_NAME=E2E
|
|
||||||
SIGN_IN_USERNAME_CHROMIUM=user.test@chromium.test
|
|
||||||
SIGN_IN_USERNAME_WEBKIT=user.test@webkit.com
|
|
||||||
SIGN_IN_USERNAME_FIREFOX=user.test@firefox.com
|
|
||||||
# Used only on instance with custom sign in
|
|
||||||
SIGN_IN_EL_USERNAME_INPUT=
|
|
||||||
SIGN_IN_EL_USERNAME_VALIDATION=
|
|
||||||
SIGN_IN_EL_PASSWORD_INPUT=
|
|
||||||
SIGN_IN_PASSWORD_CHROMIUM=
|
|
||||||
SIGN_IN_PASSWORD_WEBKIT=
|
|
||||||
SIGN_IN_PASSWORD_FIREFOX=
|
|
||||||
# To test server to server API calls
|
|
||||||
SERVER_TO_SERVER_API_TOKENS='server-api-token'
|
|
||||||
SUB_CHROMIUM=user.test@chromium.test
|
|
||||||
SUB_WEBKIT=user.test@webkit.com
|
|
||||||
SUB_FIREFOX=user.test@firefox.com
|
|
||||||
@@ -5,4 +5,3 @@ blob-report/
|
|||||||
playwright/.auth/
|
playwright/.auth/
|
||||||
playwright/.cache/
|
playwright/.cache/
|
||||||
screenshots/
|
screenshots/
|
||||||
.env.local
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FullConfig, FullProject, chromium, expect } from '@playwright/test';
|
import { FullConfig, FullProject, chromium, expect } from '@playwright/test';
|
||||||
|
|
||||||
import { SignIn } from './utils-signin';
|
import { keyCloakSignIn } from './utils-common';
|
||||||
|
|
||||||
const saveStorageState = async (
|
const saveStorageState = async (
|
||||||
browserConfig: FullProject<unknown, unknown>,
|
browserConfig: FullProject<unknown, unknown>,
|
||||||
@@ -22,7 +22,7 @@ const saveStorageState = async (
|
|||||||
await page.content();
|
await page.content();
|
||||||
await expect(page.getByText('Docs').first()).toBeVisible();
|
await expect(page.getByText('Docs').first()).toBeVisible();
|
||||||
|
|
||||||
await SignIn(page, browserName);
|
await keyCloakSignIn(page, browserName);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('header').first().getByRole('button', {
|
page.locator('header').first().getByRole('button', {
|
||||||
|
|||||||
@@ -4,166 +4,160 @@ import { expect, test } from '@playwright/test';
|
|||||||
|
|
||||||
import { CONFIG, createDoc, overrideConfig } from './utils-common';
|
import { CONFIG, createDoc, overrideConfig } from './utils-common';
|
||||||
|
|
||||||
if (process.env.TEST_INSTANCE_ONLY !== 'true') {
|
test.describe('Config', () => {
|
||||||
test.describe('Config', () => {
|
test('it checks that sentry is trying to init from config endpoint', async ({
|
||||||
test('it checks that sentry is trying to init from config endpoint', async ({
|
page,
|
||||||
page,
|
}) => {
|
||||||
}) => {
|
await overrideConfig(page, {
|
||||||
await overrideConfig(page, {
|
SENTRY_DSN: 'https://sentry.io/123',
|
||||||
SENTRY_DSN: 'https://sentry.io/123',
|
|
||||||
});
|
|
||||||
|
|
||||||
const invalidMsg = 'Invalid Sentry Dsn: https://sentry.io/123';
|
|
||||||
const consoleMessage = page.waitForEvent('console', {
|
|
||||||
timeout: 5000,
|
|
||||||
predicate: (msg) => msg.text().includes(invalidMsg),
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto('/');
|
|
||||||
|
|
||||||
expect((await consoleMessage).text()).toContain(invalidMsg);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it checks that media server is configured from config endpoint', async ({
|
const invalidMsg = 'Invalid Sentry Dsn: https://sentry.io/123';
|
||||||
page,
|
const consoleMessage = page.waitForEvent('console', {
|
||||||
browserName,
|
timeout: 5000,
|
||||||
}) => {
|
predicate: (msg) => msg.text().includes(invalidMsg),
|
||||||
await page.goto('/');
|
|
||||||
|
|
||||||
await createDoc(page, 'doc-media', browserName, 1);
|
|
||||||
|
|
||||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
|
||||||
|
|
||||||
await page.locator('.bn-block-outer').last().fill('Anything');
|
|
||||||
await page.locator('.bn-block-outer').last().fill('/');
|
|
||||||
await page.getByText('Resizable image with caption').click();
|
|
||||||
await page.getByText('Upload image').click();
|
|
||||||
|
|
||||||
const fileChooser = await fileChooserPromise;
|
|
||||||
await fileChooser.setFiles(
|
|
||||||
path.join(__dirname, 'assets/logo-suite-numerique.png'),
|
|
||||||
);
|
|
||||||
|
|
||||||
const image = page
|
|
||||||
.locator('.--docs--editor-container img.bn-visual-media')
|
|
||||||
.first();
|
|
||||||
|
|
||||||
await expect(image).toBeVisible();
|
|
||||||
|
|
||||||
// Wait for the media-check to be processed
|
|
||||||
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
// Check src of image
|
|
||||||
expect(await image.getAttribute('src')).toMatch(
|
|
||||||
/http:\/\/localhost:8083\/media\/.*\/attachments\/.*.png/,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it checks that collaboration server is configured from config endpoint', async ({
|
await page.goto('/');
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await page.goto('/');
|
|
||||||
|
|
||||||
void page
|
expect((await consoleMessage).text()).toContain(invalidMsg);
|
||||||
.getByRole('button', {
|
|
||||||
name: 'New doc',
|
|
||||||
})
|
|
||||||
.click();
|
|
||||||
|
|
||||||
const webSocket = await page.waitForEvent('websocket', (webSocket) => {
|
|
||||||
return webSocket
|
|
||||||
.url()
|
|
||||||
.includes('ws://localhost:4444/collaboration/ws/');
|
|
||||||
});
|
|
||||||
expect(webSocket.url()).toContain(
|
|
||||||
'ws://localhost:4444/collaboration/ws/',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it checks that Crisp is trying to init from config endpoint', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await overrideConfig(page, {
|
|
||||||
CRISP_WEBSITE_ID: '1234',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto('/');
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.locator('#crisp-chatbox').getByText('Invalid website'),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it checks FRONTEND_CSS_URL config', async ({ page }) => {
|
|
||||||
await overrideConfig(page, {
|
|
||||||
FRONTEND_CSS_URL: 'http://localhost:123465/css/style.css',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto('/');
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page
|
|
||||||
.locator('head link[href="http://localhost:123465/css/style.css"]')
|
|
||||||
.first(),
|
|
||||||
).toBeAttached();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it checks FRONTEND_JS_URL config', async ({ page }) => {
|
|
||||||
await overrideConfig(page, {
|
|
||||||
FRONTEND_JS_URL: 'http://localhost:123465/js/script.js',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto('/');
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page
|
|
||||||
.locator('script[src="http://localhost:123465/js/script.js"]')
|
|
||||||
.first(),
|
|
||||||
).toBeAttached();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it checks the config api is called', async ({ page }) => {
|
|
||||||
const responsePromise = page.waitForResponse(
|
|
||||||
(response) =>
|
|
||||||
response.url().includes('/config/') && response.status() === 200,
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.goto('/');
|
|
||||||
|
|
||||||
const response = await responsePromise;
|
|
||||||
expect(response.ok()).toBeTruthy();
|
|
||||||
|
|
||||||
const json = (await response.json()) as typeof CONFIG;
|
|
||||||
expect(json).toStrictEqual(CONFIG);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Config: Not logged', () => {
|
test('it checks that media server is configured from config endpoint', async ({
|
||||||
test.use({ storageState: { cookies: [], origins: [] } });
|
page,
|
||||||
|
browserName,
|
||||||
|
}) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
test('it checks that theme is configured from config endpoint', async ({
|
await createDoc(page, 'doc-media', browserName, 1);
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await page.goto('/');
|
|
||||||
|
|
||||||
await expect(
|
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||||
page.getByText('Collaborative writing, Simplified.'),
|
|
||||||
).toHaveCSS('font-family', /Roboto/i, {
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
await overrideConfig(page, {
|
await page.locator('.bn-block-outer').last().fill('Anything');
|
||||||
FRONTEND_THEME: 'dsfr',
|
await page.locator('.bn-block-outer').last().fill('/');
|
||||||
});
|
await page.getByText('Resizable image with caption').click();
|
||||||
|
await page.getByText('Upload image').click();
|
||||||
|
|
||||||
await page.goto('/');
|
const fileChooser = await fileChooserPromise;
|
||||||
|
await fileChooser.setFiles(
|
||||||
|
path.join(__dirname, 'assets/logo-suite-numerique.png'),
|
||||||
|
);
|
||||||
|
|
||||||
await expect(
|
const image = page
|
||||||
page.getByText('Collaborative writing, Simplified.'),
|
.locator('.--docs--editor-container img.bn-visual-media')
|
||||||
).toHaveCSS('font-family', /Marianne/i, {
|
.first();
|
||||||
timeout: 10000,
|
|
||||||
});
|
await expect(image).toBeVisible();
|
||||||
|
|
||||||
|
// Wait for the media-check to be processed
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Check src of image
|
||||||
|
expect(await image.getAttribute('src')).toMatch(
|
||||||
|
/http:\/\/localhost:8083\/media\/.*\/attachments\/.*.png/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it checks that collaboration server is configured from config endpoint', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
void page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'New doc',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
const webSocket = await page.waitForEvent('websocket', (webSocket) => {
|
||||||
|
return webSocket.url().includes('ws://localhost:4444/collaboration/ws/');
|
||||||
|
});
|
||||||
|
expect(webSocket.url()).toContain('ws://localhost:4444/collaboration/ws/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it checks that Crisp is trying to init from config endpoint', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await overrideConfig(page, {
|
||||||
|
CRISP_WEBSITE_ID: '1234',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.locator('#crisp-chatbox').getByText('Invalid website'),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it checks FRONTEND_CSS_URL config', async ({ page }) => {
|
||||||
|
await overrideConfig(page, {
|
||||||
|
FRONTEND_CSS_URL: 'http://localhost:123465/css/style.css',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.locator('head link[href="http://localhost:123465/css/style.css"]')
|
||||||
|
.first(),
|
||||||
|
).toBeAttached();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it checks FRONTEND_JS_URL config', async ({ page }) => {
|
||||||
|
await overrideConfig(page, {
|
||||||
|
FRONTEND_JS_URL: 'http://localhost:123465/js/script.js',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.locator('script[src="http://localhost:123465/js/script.js"]')
|
||||||
|
.first(),
|
||||||
|
).toBeAttached();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it checks the config api is called', async ({ page }) => {
|
||||||
|
const responsePromise = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/config/') && response.status() === 200,
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
const response = await responsePromise;
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
const json = (await response.json()) as typeof CONFIG;
|
||||||
|
expect(json).toStrictEqual(CONFIG);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Config: Not logged', () => {
|
||||||
|
test.use({ storageState: { cookies: [], origins: [] } });
|
||||||
|
|
||||||
|
test('it checks that theme is configured from config endpoint', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByText('Collaborative writing, Simplified.'),
|
||||||
|
).toHaveCSS('font-family', /Roboto/i, {
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await overrideConfig(page, {
|
||||||
|
FRONTEND_THEME: 'dsfr',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByText('Collaborative writing, Simplified.'),
|
||||||
|
).toHaveCSS('font-family', /Marianne/i, {
|
||||||
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|||||||
@@ -13,295 +13,210 @@ import {
|
|||||||
writeInEditor,
|
writeInEditor,
|
||||||
} from './utils-editor';
|
} from './utils-editor';
|
||||||
|
|
||||||
if (process.env.TEST_INSTANCE_ONLY !== 'true') {
|
test.beforeEach(async ({ page }) => {
|
||||||
test.describe('Doc AI feature', () => {
|
await page.goto('/');
|
||||||
test.beforeEach(async ({ page }) => {
|
});
|
||||||
await page.goto('/');
|
|
||||||
});
|
|
||||||
|
|
||||||
[
|
test.describe('Doc AI feature', () => {
|
||||||
{
|
[
|
||||||
AI_FEATURE_ENABLED: false,
|
{
|
||||||
selector: 'Ask AI',
|
AI_FEATURE_ENABLED: false,
|
||||||
},
|
selector: 'Ask AI',
|
||||||
{
|
},
|
||||||
AI_FEATURE_ENABLED: true,
|
{
|
||||||
AI_FEATURE_BLOCKNOTE_ENABLED: false,
|
AI_FEATURE_ENABLED: true,
|
||||||
selector: 'Ask AI',
|
AI_FEATURE_BLOCKNOTE_ENABLED: false,
|
||||||
},
|
selector: 'Ask AI',
|
||||||
{
|
},
|
||||||
AI_FEATURE_ENABLED: true,
|
{
|
||||||
AI_FEATURE_LEGACY_ENABLED: false,
|
AI_FEATURE_ENABLED: true,
|
||||||
selector: 'AI',
|
AI_FEATURE_LEGACY_ENABLED: false,
|
||||||
},
|
selector: 'AI',
|
||||||
].forEach((config) => {
|
},
|
||||||
test(`it checks the AI feature flag from config endpoint: ${JSON.stringify(config)}`, async ({
|
].forEach((config) => {
|
||||||
page,
|
test(`it checks the AI feature flag from config endpoint: ${JSON.stringify(config)}`, async ({
|
||||||
browserName,
|
|
||||||
}) => {
|
|
||||||
await overrideConfig(page, config);
|
|
||||||
|
|
||||||
await page.goto('/');
|
|
||||||
|
|
||||||
await createDoc(page, 'doc-ai-feature', browserName, 1);
|
|
||||||
|
|
||||||
await page.locator('.bn-block-outer').last().fill('Anything');
|
|
||||||
await page.getByText('Anything').selectText();
|
|
||||||
await expect(
|
|
||||||
page.locator('button[data-test="convertMarkdown"]'),
|
|
||||||
).toHaveCount(1);
|
|
||||||
await expect(
|
|
||||||
page.getByRole('button', { name: config.selector, exact: true }),
|
|
||||||
).toBeHidden();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it checks the AI feature and accepts changes', async ({
|
|
||||||
page,
|
page,
|
||||||
browserName,
|
browserName,
|
||||||
}) => {
|
}) => {
|
||||||
await overrideConfig(page, {
|
await overrideConfig(page, config);
|
||||||
AI_BOT: {
|
|
||||||
name: 'Albert AI',
|
|
||||||
color: '#8bc6ff',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await mockAIResponse(page);
|
|
||||||
|
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
await createDoc(page, 'doc-ai', browserName, 1);
|
await createDoc(page, 'doc-ai-feature', browserName, 1);
|
||||||
|
|
||||||
await openSuggestionMenu({ page });
|
await page.locator('.bn-block-outer').last().fill('Anything');
|
||||||
await page.getByText('Ask AI').click();
|
await page.getByText('Anything').selectText();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('option', { name: 'Continue Writing' }),
|
page.locator('button[data-test="convertMarkdown"]'),
|
||||||
).toBeVisible();
|
).toHaveCount(1);
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('option', { name: 'Summarize' }),
|
page.getByRole('button', { name: config.selector, exact: true }),
|
||||||
).toBeVisible();
|
).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
await page.keyboard.press('Escape');
|
test('it checks the AI feature and accepts changes', async ({
|
||||||
|
page,
|
||||||
const editor = await writeInEditor({ page, text: 'Hello World' });
|
browserName,
|
||||||
await editor.getByText('Hello World').selectText();
|
}) => {
|
||||||
|
await overrideConfig(page, {
|
||||||
// Check from toolbar
|
AI_BOT: {
|
||||||
await page.getByRole('button', { name: 'Ask AI' }).click();
|
name: 'Albert AI',
|
||||||
|
color: '#8bc6ff',
|
||||||
await expect(
|
},
|
||||||
page.getByRole('option', { name: 'Improve Writing' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByRole('option', { name: 'Fix Spelling' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByRole('option', { name: 'Translate' }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('option', { name: 'Translate' }).click();
|
|
||||||
await page
|
|
||||||
.getByRole('textbox', { name: 'Ask anything...' })
|
|
||||||
.fill('Translate into french');
|
|
||||||
await page
|
|
||||||
.getByRole('textbox', { name: 'Ask anything...' })
|
|
||||||
.press('Enter');
|
|
||||||
await expect(editor.getByText('Albert AI')).toBeVisible();
|
|
||||||
await page
|
|
||||||
.locator('p.bn-mt-suggestion-menu-item-title')
|
|
||||||
.getByText('Accept')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
|
|
||||||
|
|
||||||
// Check Suggestion menu
|
|
||||||
await page.locator('.bn-block-outer').last().fill('/');
|
|
||||||
await expect(page.getByText('Write with AI')).toBeVisible();
|
|
||||||
|
|
||||||
// Reload the page to check that the AI change is still there
|
|
||||||
await page.goto(page.url());
|
|
||||||
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it reverts with the AI feature', async ({ page, browserName }) => {
|
await mockAIResponse(page);
|
||||||
await overrideConfig(page, {
|
|
||||||
AI_BOT: {
|
|
||||||
name: 'Albert AI',
|
|
||||||
color: '#8bc6ff',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await mockAIResponse(page);
|
await page.goto('/');
|
||||||
|
|
||||||
await page.goto('/');
|
await createDoc(page, 'doc-ai', browserName, 1);
|
||||||
|
|
||||||
await createDoc(page, 'doc-ai', browserName, 1);
|
await openSuggestionMenu({ page });
|
||||||
|
await page.getByText('Ask AI').click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('option', { name: 'Continue Writing' }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.getByRole('option', { name: 'Summarize' })).toBeVisible();
|
||||||
|
|
||||||
const editor = await writeInEditor({ page, text: 'Hello World' });
|
await page.keyboard.press('Escape');
|
||||||
await editor.getByText('Hello World').selectText();
|
|
||||||
|
|
||||||
// Check from toolbar
|
const editor = await writeInEditor({ page, text: 'Hello World' });
|
||||||
await page.getByRole('button', { name: 'Ask AI' }).click();
|
await editor.getByText('Hello World').selectText();
|
||||||
|
|
||||||
await page.getByRole('option', { name: 'Translate' }).click();
|
// Check from toolbar
|
||||||
await page
|
await page.getByRole('button', { name: 'Ask AI' }).click();
|
||||||
.getByRole('textbox', { name: 'Ask anything...' })
|
|
||||||
.fill('Translate into french');
|
|
||||||
await page
|
|
||||||
.getByRole('textbox', { name: 'Ask anything...' })
|
|
||||||
.press('Enter');
|
|
||||||
await expect(editor.getByText('Albert AI')).toBeVisible();
|
|
||||||
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
|
|
||||||
await page
|
|
||||||
.locator('p.bn-mt-suggestion-menu-item-title')
|
|
||||||
.getByText('Revert')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(editor.getByText('Hello World')).toBeVisible();
|
await expect(
|
||||||
|
page.getByRole('option', { name: 'Improve Writing' }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('option', { name: 'Fix Spelling' }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.getByRole('option', { name: 'Translate' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('option', { name: 'Translate' }).click();
|
||||||
|
await page
|
||||||
|
.getByRole('textbox', { name: 'Ask anything...' })
|
||||||
|
.fill('Translate into french');
|
||||||
|
await page.getByRole('textbox', { name: 'Ask anything...' }).press('Enter');
|
||||||
|
await expect(editor.getByText('Albert AI')).toBeVisible();
|
||||||
|
await page
|
||||||
|
.locator('p.bn-mt-suggestion-menu-item-title')
|
||||||
|
.getByText('Accept')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
|
||||||
|
|
||||||
|
// Check Suggestion menu
|
||||||
|
await page.locator('.bn-block-outer').last().fill('/');
|
||||||
|
await expect(page.getByText('Write with AI')).toBeVisible();
|
||||||
|
|
||||||
|
// Reload the page to check that the AI change is still there
|
||||||
|
await page.goto(page.url());
|
||||||
|
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it reverts with the AI feature', async ({ page, browserName }) => {
|
||||||
|
await overrideConfig(page, {
|
||||||
|
AI_BOT: {
|
||||||
|
name: 'Albert AI',
|
||||||
|
color: '#8bc6ff',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it checks the AI buttons feature legacy', async ({
|
await mockAIResponse(page);
|
||||||
page,
|
|
||||||
browserName,
|
|
||||||
}) => {
|
|
||||||
await page.route(/.*\/ai-translate\//, async (route) => {
|
|
||||||
const request = route.request();
|
|
||||||
if (request.method().includes('POST')) {
|
|
||||||
await route.fulfill({
|
|
||||||
json: {
|
|
||||||
answer: 'Hallo Welt',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await route.continue();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await createDoc(page, 'doc-ai', browserName, 1);
|
await page.goto('/');
|
||||||
|
|
||||||
await page.locator('.bn-block-outer').last().fill('Hello World');
|
await createDoc(page, 'doc-ai', browserName, 1);
|
||||||
|
|
||||||
const editor = page.locator('.ProseMirror');
|
const editor = await writeInEditor({ page, text: 'Hello World' });
|
||||||
await editor.getByText('Hello').selectText();
|
await editor.getByText('Hello World').selectText();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'AI', exact: true }).click();
|
// Check from toolbar
|
||||||
|
await page.getByRole('button', { name: 'Ask AI' }).click();
|
||||||
|
|
||||||
await expect(
|
await page.getByRole('option', { name: 'Translate' }).click();
|
||||||
page.getByRole('menuitem', { name: 'Use as prompt' }),
|
await page
|
||||||
).toBeVisible();
|
.getByRole('textbox', { name: 'Ask anything...' })
|
||||||
await expect(
|
.fill('Translate into french');
|
||||||
page.getByRole('menuitem', { name: 'Rephrase' }),
|
await page.getByRole('textbox', { name: 'Ask anything...' }).press('Enter');
|
||||||
).toBeVisible();
|
await expect(editor.getByText('Albert AI')).toBeVisible();
|
||||||
await expect(
|
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
|
||||||
page.getByRole('menuitem', { name: 'Summarize' }),
|
await page
|
||||||
).toBeVisible();
|
.locator('p.bn-mt-suggestion-menu-item-title')
|
||||||
await expect(
|
.getByText('Revert')
|
||||||
page.getByRole('menuitem', { name: 'Correct' }),
|
.click();
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByRole('menuitem', { name: 'Language' }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Language' }).hover();
|
await expect(editor.getByText('Hello World')).toBeVisible();
|
||||||
await expect(
|
});
|
||||||
page.getByRole('menuitem', { name: 'English', exact: true }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByRole('menuitem', { name: 'French', exact: true }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByRole('menuitem', { name: 'German', exact: true }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'German', exact: true }).click();
|
test('it checks the AI buttons feature legacy', async ({
|
||||||
|
page,
|
||||||
await expect(editor.getByText('Hallo Welt')).toBeVisible();
|
browserName,
|
||||||
});
|
}) => {
|
||||||
|
await page.route(/.*\/ai-translate\//, async (route) => {
|
||||||
[
|
const request = route.request();
|
||||||
{ ai_transform: false, ai_translate: false },
|
if (request.method().includes('POST')) {
|
||||||
{ ai_transform: true, ai_translate: false },
|
await route.fulfill({
|
||||||
{ ai_transform: false, ai_translate: true },
|
json: {
|
||||||
].forEach(({ ai_transform, ai_translate }) => {
|
answer: 'Hallo Welt',
|
||||||
test(`it checks AI buttons when can transform is at "${ai_transform}" and can translate is at "${ai_translate}"`, async ({
|
|
||||||
page,
|
|
||||||
browserName,
|
|
||||||
}) => {
|
|
||||||
await mockedDocument(page, {
|
|
||||||
accesses: [
|
|
||||||
{
|
|
||||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
|
||||||
role: 'owner',
|
|
||||||
user: {
|
|
||||||
email: 'super@owner.com',
|
|
||||||
full_name: 'Super Owner',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
abilities: {
|
|
||||||
destroy: true, // Means owner
|
|
||||||
link_configuration: true,
|
|
||||||
ai_transform,
|
|
||||||
ai_translate,
|
|
||||||
accesses_manage: true,
|
|
||||||
accesses_view: true,
|
|
||||||
update: true,
|
|
||||||
partial_update: true,
|
|
||||||
retrieve: true,
|
|
||||||
},
|
},
|
||||||
link_reach: 'restricted',
|
|
||||||
link_role: 'editor',
|
|
||||||
created_at: '2021-09-01T09:00:00Z',
|
|
||||||
title: '',
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
const [randomDoc] = await createDoc(
|
await route.continue();
|
||||||
page,
|
}
|
||||||
'doc-editor-ai',
|
|
||||||
browserName,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
|
|
||||||
await verifyDocName(page, randomDoc);
|
|
||||||
|
|
||||||
await page.locator('.bn-block-outer').last().fill('Hello World');
|
|
||||||
|
|
||||||
const editor = page.locator('.ProseMirror');
|
|
||||||
await editor.getByText('Hello').selectText();
|
|
||||||
|
|
||||||
if (!ai_transform && !ai_translate) {
|
|
||||||
await expect(
|
|
||||||
page.getByRole('button', { name: 'AI', exact: true }),
|
|
||||||
).toBeHidden();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'AI', exact: true }).click();
|
|
||||||
|
|
||||||
if (ai_transform) {
|
|
||||||
await expect(
|
|
||||||
page.getByRole('menuitem', { name: 'Use as prompt' }),
|
|
||||||
).toBeVisible();
|
|
||||||
} else {
|
|
||||||
await expect(
|
|
||||||
page.getByRole('menuitem', { name: 'Use as prompt' }),
|
|
||||||
).toBeHidden();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ai_translate) {
|
|
||||||
await expect(
|
|
||||||
page.getByRole('menuitem', { name: 'Language' }),
|
|
||||||
).toBeVisible();
|
|
||||||
} else {
|
|
||||||
await expect(
|
|
||||||
page.getByRole('menuitem', { name: 'Language' }),
|
|
||||||
).toBeHidden();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`it checks ai_proxy ability`, async ({ page, browserName }) => {
|
await createDoc(page, 'doc-ai', browserName, 1);
|
||||||
|
|
||||||
|
await page.locator('.bn-block-outer').last().fill('Hello World');
|
||||||
|
|
||||||
|
const editor = page.locator('.ProseMirror');
|
||||||
|
await editor.getByText('Hello').selectText();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'AI', exact: true }).click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('menuitem', { name: 'Use as prompt' }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('menuitem', { name: 'Rephrase' }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('menuitem', { name: 'Summarize' }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Correct' })).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('menuitem', { name: 'Language' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('menuitem', { name: 'Language' }).hover();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('menuitem', { name: 'English', exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('menuitem', { name: 'French', exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('menuitem', { name: 'German', exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('menuitem', { name: 'German', exact: true }).click();
|
||||||
|
|
||||||
|
await expect(editor.getByText('Hallo Welt')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
{ ai_transform: false, ai_translate: false },
|
||||||
|
{ ai_transform: true, ai_translate: false },
|
||||||
|
{ ai_transform: false, ai_translate: true },
|
||||||
|
].forEach(({ ai_transform, ai_translate }) => {
|
||||||
|
test(`it checks AI buttons when can transform is at "${ai_transform}" and can translate is at "${ai_translate}"`, async ({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
}) => {
|
||||||
await mockedDocument(page, {
|
await mockedDocument(page, {
|
||||||
accesses: [
|
accesses: [
|
||||||
{
|
{
|
||||||
@@ -316,7 +231,8 @@ if (process.env.TEST_INSTANCE_ONLY !== 'true') {
|
|||||||
abilities: {
|
abilities: {
|
||||||
destroy: true, // Means owner
|
destroy: true, // Means owner
|
||||||
link_configuration: true,
|
link_configuration: true,
|
||||||
ai_proxy: false,
|
ai_transform,
|
||||||
|
ai_translate,
|
||||||
accesses_manage: true,
|
accesses_manage: true,
|
||||||
accesses_view: true,
|
accesses_view: true,
|
||||||
update: true,
|
update: true,
|
||||||
@@ -331,7 +247,7 @@ if (process.env.TEST_INSTANCE_ONLY !== 'true') {
|
|||||||
|
|
||||||
const [randomDoc] = await createDoc(
|
const [randomDoc] = await createDoc(
|
||||||
page,
|
page,
|
||||||
'doc-editor-ai-proxy',
|
'doc-editor-ai',
|
||||||
browserName,
|
browserName,
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
@@ -343,9 +259,81 @@ if (process.env.TEST_INSTANCE_ONLY !== 'true') {
|
|||||||
const editor = page.locator('.ProseMirror');
|
const editor = page.locator('.ProseMirror');
|
||||||
await editor.getByText('Hello').selectText();
|
await editor.getByText('Hello').selectText();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Ask AI' })).toBeHidden();
|
if (!ai_transform && !ai_translate) {
|
||||||
await page.locator('.bn-block-outer').last().fill('/');
|
await expect(
|
||||||
await expect(page.getByText('Write with AI')).toBeHidden();
|
page.getByRole('button', { name: 'AI', exact: true }),
|
||||||
|
).toBeHidden();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'AI', exact: true }).click();
|
||||||
|
|
||||||
|
if (ai_transform) {
|
||||||
|
await expect(
|
||||||
|
page.getByRole('menuitem', { name: 'Use as prompt' }),
|
||||||
|
).toBeVisible();
|
||||||
|
} else {
|
||||||
|
await expect(
|
||||||
|
page.getByRole('menuitem', { name: 'Use as prompt' }),
|
||||||
|
).toBeHidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ai_translate) {
|
||||||
|
await expect(
|
||||||
|
page.getByRole('menuitem', { name: 'Language' }),
|
||||||
|
).toBeVisible();
|
||||||
|
} else {
|
||||||
|
await expect(
|
||||||
|
page.getByRole('menuitem', { name: 'Language' }),
|
||||||
|
).toBeHidden();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
test(`it checks ai_proxy ability`, async ({ page, browserName }) => {
|
||||||
|
await mockedDocument(page, {
|
||||||
|
accesses: [
|
||||||
|
{
|
||||||
|
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||||
|
role: 'owner',
|
||||||
|
user: {
|
||||||
|
email: 'super@owner.com',
|
||||||
|
full_name: 'Super Owner',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
abilities: {
|
||||||
|
destroy: true, // Means owner
|
||||||
|
link_configuration: true,
|
||||||
|
ai_proxy: false,
|
||||||
|
accesses_manage: true,
|
||||||
|
accesses_view: true,
|
||||||
|
update: true,
|
||||||
|
partial_update: true,
|
||||||
|
retrieve: true,
|
||||||
|
},
|
||||||
|
link_reach: 'restricted',
|
||||||
|
link_role: 'editor',
|
||||||
|
created_at: '2021-09-01T09:00:00Z',
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [randomDoc] = await createDoc(
|
||||||
|
page,
|
||||||
|
'doc-editor-ai-proxy',
|
||||||
|
browserName,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
await verifyDocName(page, randomDoc);
|
||||||
|
|
||||||
|
await page.locator('.bn-block-outer').last().fill('Hello World');
|
||||||
|
|
||||||
|
const editor = page.locator('.ProseMirror');
|
||||||
|
await editor.getByText('Hello').selectText();
|
||||||
|
|
||||||
|
await expect(page.getByRole('button', { name: 'Ask AI' })).toBeHidden();
|
||||||
|
await page.locator('.bn-block-outer').last().fill('/');
|
||||||
|
await expect(page.getByText('Write with AI')).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -58,14 +58,10 @@ test.describe('Doc Comments', () => {
|
|||||||
await page.getByRole('button', { name: '👍' }).click();
|
await page.getByRole('button', { name: '👍' }).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
thread
|
thread.getByRole('img', { name: `E2E ${browserName}` }).first(),
|
||||||
.getByRole('img', { name: `${process.env.FIRST_NAME} ${browserName}` })
|
|
||||||
.first(),
|
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(thread.getByText('This is a comment').first()).toBeVisible();
|
await expect(thread.getByText('This is a comment').first()).toBeVisible();
|
||||||
await expect(
|
await expect(thread.getByText(`E2E ${browserName}`).first()).toBeVisible();
|
||||||
thread.getByText(`${process.env.FIRST_NAME} ${browserName}`).first(),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(thread.locator('.bn-comment-reaction')).toHaveText('👍1');
|
await expect(thread.locator('.bn-comment-reaction')).toHaveText('👍1');
|
||||||
|
|
||||||
const urlCommentDoc = page.url();
|
const urlCommentDoc = page.url();
|
||||||
@@ -89,7 +85,7 @@ test.describe('Doc Comments', () => {
|
|||||||
otherThread.getByText('This is a comment').first(),
|
otherThread.getByText('This is a comment').first(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
otherThread.getByText(`${process.env.FIRST_NAME} ${browserName}`).first(),
|
otherThread.getByText(`E2E ${browserName}`).first(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(otherThread.locator('.bn-comment-reaction')).toHaveText('👍2');
|
await expect(otherThread.locator('.bn-comment-reaction')).toHaveText('👍2');
|
||||||
|
|
||||||
@@ -102,19 +98,13 @@ test.describe('Doc Comments', () => {
|
|||||||
|
|
||||||
// We check that the second user can see the comment he just made
|
// We check that the second user can see the comment he just made
|
||||||
await expect(
|
await expect(
|
||||||
otherThread
|
otherThread.getByRole('img', { name: `E2E ${otherBrowserName}` }).first(),
|
||||||
.getByRole('img', {
|
|
||||||
name: `${process.env.FIRST_NAME} ${otherBrowserName}`,
|
|
||||||
})
|
|
||||||
.first(),
|
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
otherThread.getByText('This is a comment from the other user').first(),
|
otherThread.getByText('This is a comment from the other user').first(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
otherThread
|
otherThread.getByText(`E2E ${otherBrowserName}`).first(),
|
||||||
.getByText(`${process.env.FIRST_NAME} ${otherBrowserName}`)
|
|
||||||
.first(),
|
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// We check that the first user can see the comment made by the second user in real time
|
// We check that the first user can see the comment made by the second user in real time
|
||||||
@@ -122,7 +112,7 @@ test.describe('Doc Comments', () => {
|
|||||||
thread.getByText('This is a comment from the other user').first(),
|
thread.getByText('This is a comment from the other user').first(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
thread.getByText(`${process.env.FIRST_NAME} ${otherBrowserName}`).first(),
|
thread.getByText(`E2E ${otherBrowserName}`).first(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await cleanup();
|
await cleanup();
|
||||||
@@ -144,7 +134,7 @@ test.describe('Doc Comments', () => {
|
|||||||
|
|
||||||
await expect(editor.getByText('Hello')).toHaveCSS(
|
await expect(editor.getByText('Hello')).toHaveCSS(
|
||||||
'background-color',
|
'background-color',
|
||||||
/color\(srgb\s+[\d\s.]+\s+\/\s+0\.4\)/,
|
'color(srgb 0.882353 0.831373 0.717647 / 0.4)',
|
||||||
);
|
);
|
||||||
|
|
||||||
await editor.first().click();
|
await editor.first().click();
|
||||||
@@ -211,7 +201,7 @@ test.describe('Doc Comments', () => {
|
|||||||
|
|
||||||
await expect(editor.getByText('Hello')).toHaveCSS(
|
await expect(editor.getByText('Hello')).toHaveCSS(
|
||||||
'background-color',
|
'background-color',
|
||||||
/color\(srgb\s+[\d\s.]+\s+\/\s+0\.4\)/,
|
'color(srgb 0.882353 0.831373 0.717647 / 0.4)',
|
||||||
);
|
);
|
||||||
|
|
||||||
await editor.first().click();
|
await editor.first().click();
|
||||||
@@ -277,15 +267,11 @@ test.describe('Doc Comments', () => {
|
|||||||
|
|
||||||
await expect(otherEditor.getByText('Hello')).toHaveCSS(
|
await expect(otherEditor.getByText('Hello')).toHaveCSS(
|
||||||
'background-color',
|
'background-color',
|
||||||
/color\(srgb\s+[\d\s.]+\s+\/\s+0\.4\)/,
|
'color(srgb 0.882353 0.831373 0.717647 / 0.4)',
|
||||||
);
|
);
|
||||||
|
|
||||||
// We change the role of the second user to reader
|
// We change the role of the second user to reader
|
||||||
await updateRoleUser(
|
await updateRoleUser(page, 'Reader', `user.test@${otherBrowserName}.test`);
|
||||||
page,
|
|
||||||
'Reader',
|
|
||||||
process.env[`SIGN_IN_USERNAME_${otherBrowserName.toUpperCase()}`] || '',
|
|
||||||
);
|
|
||||||
|
|
||||||
// With the reader role, the second user cannot see comments
|
// With the reader role, the second user cannot see comments
|
||||||
await otherPage.reload();
|
await otherPage.reload();
|
||||||
@@ -310,15 +296,13 @@ test.describe('Doc Comments', () => {
|
|||||||
// Anonymous user can see and add comments
|
// Anonymous user can see and add comments
|
||||||
await otherPage.getByRole('button', { name: 'Logout' }).click();
|
await otherPage.getByRole('button', { name: 'Logout' }).click();
|
||||||
|
|
||||||
await otherPage.waitForTimeout(1500);
|
|
||||||
|
|
||||||
await otherPage.goto(urlCommentDoc);
|
await otherPage.goto(urlCommentDoc);
|
||||||
|
|
||||||
await verifyDocName(otherPage, docTitle);
|
await verifyDocName(otherPage, docTitle);
|
||||||
|
|
||||||
await expect(otherEditor.getByText('Hello')).toHaveCSS(
|
await expect(otherEditor.getByText('Hello')).toHaveCSS(
|
||||||
'background-color',
|
'background-color',
|
||||||
/color\(srgb\s+[\d\s.]+\s+\/\s+0\.4\)/,
|
'color(srgb 0.882353 0.831373 0.717647 / 0.4)',
|
||||||
);
|
);
|
||||||
await otherEditor.getByText('Hello').click();
|
await otherEditor.getByText('Hello').click();
|
||||||
await expect(
|
await expect(
|
||||||
@@ -364,7 +348,7 @@ test.describe('Doc Comments', () => {
|
|||||||
|
|
||||||
await expect(editor1.getByText('Document One')).toHaveCSS(
|
await expect(editor1.getByText('Document One')).toHaveCSS(
|
||||||
'background-color',
|
'background-color',
|
||||||
/color\(srgb\s+[\d\s.]+\s+\/\s+0\.4\)/,
|
'color(srgb 0.882353 0.831373 0.717647 / 0.4)',
|
||||||
);
|
);
|
||||||
|
|
||||||
await editor1.getByText('Document One').click();
|
await editor1.getByText('Document One').click();
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { expect, test } from '@playwright/test';
|
|||||||
import {
|
import {
|
||||||
createDoc,
|
createDoc,
|
||||||
goToGridDoc,
|
goToGridDoc,
|
||||||
|
keyCloakSignIn,
|
||||||
randomName,
|
randomName,
|
||||||
verifyDocName,
|
verifyDocName,
|
||||||
} from './utils-common';
|
} from './utils-common';
|
||||||
import { connectOtherUserToDoc } from './utils-share';
|
import { connectOtherUserToDoc } from './utils-share';
|
||||||
import { SignIn } from './utils-signin';
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -22,7 +22,8 @@ test.describe('Doc Create', () => {
|
|||||||
{ timeout: 5000 },
|
{ timeout: 5000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Back to homepage' }).click();
|
const header = page.locator('header').first();
|
||||||
|
await header.locator('h1').getByText('Docs').click();
|
||||||
|
|
||||||
const docsGrid = page.getByTestId('docs-grid');
|
const docsGrid = page.getByTestId('docs-grid');
|
||||||
await expect(docsGrid).toBeVisible();
|
await expect(docsGrid).toBeVisible();
|
||||||
@@ -133,7 +134,7 @@ test.describe('Doc Create', () => {
|
|||||||
withoutSignIn: true,
|
withoutSignIn: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await SignIn(otherPage, otherBrowserName, false);
|
await keyCloakSignIn(otherPage, otherBrowserName, false);
|
||||||
|
|
||||||
await verifyDocName(otherPage, 'From unlogged doc from url');
|
await verifyDocName(otherPage, 'From unlogged doc from url');
|
||||||
|
|
||||||
@@ -159,21 +160,22 @@ test.describe('Doc Create: Not logged', () => {
|
|||||||
browserName,
|
browserName,
|
||||||
request,
|
request,
|
||||||
}) => {
|
}) => {
|
||||||
|
const SERVER_TO_SERVER_API_TOKENS = 'server-api-token';
|
||||||
const markdown = `This is a normal text\n\n# And this is a large heading`;
|
const markdown = `This is a normal text\n\n# And this is a large heading`;
|
||||||
const [title] = randomName('My server way doc create', browserName, 1);
|
const [title] = randomName('My server way doc create', browserName, 1);
|
||||||
const data = {
|
const data = {
|
||||||
title,
|
title,
|
||||||
content: markdown,
|
content: markdown,
|
||||||
sub: process.env[`SUB_${browserName.toUpperCase()}`],
|
sub: `user.test@${browserName}.test`,
|
||||||
email: process.env[`SIGN_IN_USERNAME_${browserName.toUpperCase()}`],
|
email: `user.test@${browserName}.test`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newDoc = await request.post(
|
const newDoc = await request.post(
|
||||||
`${process.env.BASE_API_URL}/documents/create-for-owner/`,
|
`http://localhost:8071/api/v1.0/documents/create-for-owner/`,
|
||||||
{
|
{
|
||||||
data,
|
data,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${process.env.SERVER_TO_SERVER_API_TOKENS}`,
|
Authorization: `Bearer ${SERVER_TO_SERVER_API_TOKENS}`,
|
||||||
format: 'json',
|
format: 'json',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -181,7 +183,7 @@ test.describe('Doc Create: Not logged', () => {
|
|||||||
|
|
||||||
expect(newDoc.ok()).toBeTruthy();
|
expect(newDoc.ok()).toBeTruthy();
|
||||||
|
|
||||||
await SignIn(page, browserName);
|
await keyCloakSignIn(page, browserName);
|
||||||
|
|
||||||
await goToGridDoc(page, { title });
|
await goToGridDoc(page, { title });
|
||||||
|
|
||||||
|
|||||||
@@ -208,11 +208,8 @@ export const goToGridDoc = async (
|
|||||||
page: Page,
|
page: Page,
|
||||||
{ nthRow = 1, title }: GoToGridDocOptions = {},
|
{ nthRow = 1, title }: GoToGridDocOptions = {},
|
||||||
) => {
|
) => {
|
||||||
if (
|
const header = page.locator('header').first();
|
||||||
await page.getByRole('button', { name: 'Back to homepage' }).isVisible()
|
await header.locator('h1').getByText('Docs').click();
|
||||||
) {
|
|
||||||
await page.getByRole('button', { name: 'Back to homepage' }).click();
|
|
||||||
}
|
|
||||||
|
|
||||||
const docsGrid = page.getByTestId('docs-grid');
|
const docsGrid = page.getByTestId('docs-grid');
|
||||||
await expect(docsGrid).toBeVisible();
|
await expect(docsGrid).toBeVisible();
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { Page, chromium, expect } from '@playwright/test';
|
|||||||
import {
|
import {
|
||||||
BrowserName,
|
BrowserName,
|
||||||
getOtherBrowserName,
|
getOtherBrowserName,
|
||||||
|
keyCloakSignIn,
|
||||||
verifyDocName,
|
verifyDocName,
|
||||||
} from './utils-common';
|
} from './utils-common';
|
||||||
import { SignIn } from './utils-signin';
|
|
||||||
|
|
||||||
export type Role = 'Administrator' | 'Owner' | 'Editor' | 'Reader';
|
export type Role = 'Administrator' | 'Owner' | 'Editor' | 'Reader';
|
||||||
export type LinkReach = 'Private' | 'Connected' | 'Public';
|
export type LinkReach = 'Private' | 'Connected' | 'Public';
|
||||||
@@ -131,14 +131,14 @@ export const connectOtherUserToDoc = async ({
|
|||||||
.getByRole('main', { name: 'Main content' })
|
.getByRole('main', { name: 'Main content' })
|
||||||
.getByLabel('Login');
|
.getByLabel('Login');
|
||||||
const loginFromHome = otherPage.getByRole('button', {
|
const loginFromHome = otherPage.getByRole('button', {
|
||||||
name: process.env.SIGN_IN_EL_TRIGGER,
|
name: 'Start Writing',
|
||||||
});
|
});
|
||||||
|
|
||||||
await loginFromApp.or(loginFromHome).first().click({
|
await loginFromApp.or(loginFromHome).first().click({
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
});
|
});
|
||||||
|
|
||||||
await SignIn(otherPage, otherBrowserName, false);
|
await keyCloakSignIn(otherPage, otherBrowserName, false);
|
||||||
}
|
}
|
||||||
if (docTitle) {
|
if (docTitle) {
|
||||||
await verifyDocName(otherPage, docTitle);
|
await verifyDocName(otherPage, docTitle);
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
import { Page, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
export const SignIn = async (
|
|
||||||
page: Page,
|
|
||||||
browserName: string,
|
|
||||||
fromHome = true,
|
|
||||||
) => {
|
|
||||||
if (process.env.CUSTOM_SIGN_IN === 'true') {
|
|
||||||
await customSignIn(page, browserName, fromHome);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await keyCloakSignIn(page, browserName, fromHome);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const customSignIn = async (
|
|
||||||
page: Page,
|
|
||||||
browserName: string,
|
|
||||||
fromHome = true,
|
|
||||||
) => {
|
|
||||||
// Check if already signed in (Silent login or session still valid)
|
|
||||||
if (
|
|
||||||
await page
|
|
||||||
.locator('header')
|
|
||||||
.first()
|
|
||||||
.getByRole('button', {
|
|
||||||
name: 'Logout',
|
|
||||||
})
|
|
||||||
.isVisible()
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fromHome) {
|
|
||||||
await page
|
|
||||||
.getByRole('button', { name: process.env.SIGN_IN_EL_TRIGGER })
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page
|
|
||||||
.getByRole('textbox', { name: process.env.SIGN_IN_EL_USERNAME_INPUT })
|
|
||||||
.fill(process.env[`SIGN_IN_USERNAME_${browserName.toUpperCase()}`] || '');
|
|
||||||
|
|
||||||
if (process.env.SIGN_IN_EL_USERNAME_VALIDATION) {
|
|
||||||
await page
|
|
||||||
.getByRole('button', { name: process.env.SIGN_IN_EL_USERNAME_VALIDATION })
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page
|
|
||||||
.locator(
|
|
||||||
`input[name="${process.env.SIGN_IN_EL_PASSWORD_INPUT || 'password'}"]`,
|
|
||||||
)
|
|
||||||
.fill(process.env[`SIGN_IN_PASSWORD_${browserName.toUpperCase()}`] || '');
|
|
||||||
|
|
||||||
await page.click('button[type="submit"]', { force: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const keyCloakSignIn = async (
|
|
||||||
page: Page,
|
|
||||||
browserName: string,
|
|
||||||
fromHome = true,
|
|
||||||
) => {
|
|
||||||
if (fromHome) {
|
|
||||||
await page
|
|
||||||
.getByRole('button', { name: process.env.SIGN_IN_EL_TRIGGER })
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
const login = `user-e2e-${browserName}`;
|
|
||||||
const password = `password-e2e-${browserName}`;
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.locator('.login-pf #kc-header-wrapper').getByText('impress'),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
if (await page.getByLabel('Restart login').isVisible()) {
|
|
||||||
await page.getByLabel('Restart login').click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.getByRole('textbox', { name: 'username' }).fill(login);
|
|
||||||
await page.getByRole('textbox', { name: 'password' }).fill(password);
|
|
||||||
await page.click('button[type="submit"]', { force: true });
|
|
||||||
};
|
|
||||||
@@ -24,7 +24,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/pngjs": "6.0.5",
|
"@types/pngjs": "6.0.5",
|
||||||
"convert-stream": "1.0.2",
|
"convert-stream": "1.0.2",
|
||||||
"dotenv": "17.3.1",
|
|
||||||
"pdf-parse": "2.4.5",
|
"pdf-parse": "2.4.5",
|
||||||
"pixelmatch": "7.1.0",
|
"pixelmatch": "7.1.0",
|
||||||
"pngjs": "7.0.0"
|
"pngjs": "7.0.0"
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
import dotenv from 'dotenv';
|
|
||||||
|
|
||||||
dotenv.config({ path: ['./.env.local', './.env'], quiet: true, debug: true });
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
const PORT = process.env.PORT;
|
const baseURL = `http://localhost:${PORT}`;
|
||||||
const baseURL = process.env.BASE_URL;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See https://playwright.dev/docs/test-configuration.
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ export const DropdownMenu = ({
|
|||||||
<Box
|
<Box
|
||||||
$theme="neutral"
|
$theme="neutral"
|
||||||
$variation={isDisabled ? 'tertiary' : 'primary'}
|
$variation={isDisabled ? 'tertiary' : 'primary'}
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{option.icon}
|
{option.icon}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -118,13 +118,13 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
const options: DropdownMenuOption[] = [
|
const options: DropdownMenuOption[] = [
|
||||||
{
|
{
|
||||||
label: t('Share'),
|
label: t('Share'),
|
||||||
icon: <GroupSVG width={24} height={24} aria-hidden="true" />,
|
icon: <GroupSVG width={24} height={24} />,
|
||||||
callback: modalShare.open,
|
callback: modalShare.open,
|
||||||
show: isSmallMobile,
|
show: isSmallMobile,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('Export'),
|
label: t('Export'),
|
||||||
icon: <DownloadSVG width={24} height={24} aria-hidden="true" />,
|
icon: <DownloadSVG width={24} height={24} />,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
setIsModalExportOpen(true);
|
setIsModalExportOpen(true);
|
||||||
},
|
},
|
||||||
@@ -133,9 +133,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
{
|
{
|
||||||
label: doc.is_favorite ? t('Unpin') : t('Pin'),
|
label: doc.is_favorite ? t('Unpin') : t('Pin'),
|
||||||
icon: doc.is_favorite ? (
|
icon: doc.is_favorite ? (
|
||||||
<KeepOffSVG width={24} height={24} aria-hidden="true" />
|
<KeepOffSVG width={24} height={24} />
|
||||||
) : (
|
) : (
|
||||||
<KeepSVG width={24} height={24} aria-hidden="true" />
|
<KeepSVG width={24} height={24} />
|
||||||
),
|
),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
if (doc.is_favorite) {
|
if (doc.is_favorite) {
|
||||||
@@ -148,7 +148,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('Version history'),
|
label: t('Version history'),
|
||||||
icon: <HistorySVG width={24} height={24} aria-hidden="true" />,
|
icon: <HistorySVG width={24} height={24} />,
|
||||||
disabled: !doc.abilities.versions_list,
|
disabled: !doc.abilities.versions_list,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
selectHistoryModal.open();
|
selectHistoryModal.open();
|
||||||
@@ -158,7 +158,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('Remove emoji'),
|
label: t('Remove emoji'),
|
||||||
icon: <RemoveEmojiSVG width={24} height={24} aria-hidden="true" />,
|
icon: <RemoveEmojiSVG width={24} height={24} />,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
updateDocEmoji(doc.id, doc.title ?? '', '');
|
updateDocEmoji(doc.id, doc.title ?? '', '');
|
||||||
},
|
},
|
||||||
@@ -167,7 +167,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('Add emoji'),
|
label: t('Add emoji'),
|
||||||
icon: <AddEmojiSVG width={24} height={24} aria-hidden="true" />,
|
icon: <AddEmojiSVG width={24} height={24} />,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
updateDocEmoji(doc.id, doc.title ?? '', '📄');
|
updateDocEmoji(doc.id, doc.title ?? '', '📄');
|
||||||
},
|
},
|
||||||
@@ -176,12 +176,12 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('Copy link'),
|
label: t('Copy link'),
|
||||||
icon: <AddLinkSVG width={24} height={24} aria-hidden="true" />,
|
icon: <AddLinkSVG width={24} height={24} />,
|
||||||
callback: copyDocLink,
|
callback: copyDocLink,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('Copy as {{format}}', { format: 'Markdown' }),
|
label: t('Copy as {{format}}', { format: 'Markdown' }),
|
||||||
icon: <MarkdownCopySVG width={24} height={24} aria-hidden="true" />,
|
icon: <MarkdownCopySVG width={24} height={24} />,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
void copyCurrentEditorToClipboard('markdown');
|
void copyCurrentEditorToClipboard('markdown');
|
||||||
},
|
},
|
||||||
@@ -189,7 +189,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('Duplicate'),
|
label: t('Duplicate'),
|
||||||
icon: <ContentCopySVG width={24} height={24} aria-hidden="true" />,
|
icon: <ContentCopySVG width={24} height={24} />,
|
||||||
disabled: !doc.abilities.duplicate,
|
disabled: !doc.abilities.duplicate,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
duplicateDoc({
|
duplicateDoc({
|
||||||
@@ -202,7 +202,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: isChild ? t('Delete sub-document') : t('Delete document'),
|
label: isChild ? t('Delete sub-document') : t('Delete document'),
|
||||||
icon: <DeleteSVG width={24} height={24} aria-hidden="true" />,
|
icon: <DeleteSVG width={24} height={24} />,
|
||||||
disabled: !doc.abilities.destroy,
|
disabled: !doc.abilities.destroy,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
setIsModalRemoveOpen(true);
|
setIsModalRemoveOpen(true);
|
||||||
|
|||||||
+6
-6
@@ -72,9 +72,9 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
|||||||
{
|
{
|
||||||
label: doc.is_favorite ? t('Unpin') : t('Pin'),
|
label: doc.is_favorite ? t('Unpin') : t('Pin'),
|
||||||
icon: doc.is_favorite ? (
|
icon: doc.is_favorite ? (
|
||||||
<KeepOffSVG width={24} height={24} aria-hidden="true" />
|
<KeepOffSVG width={24} height={24} />
|
||||||
) : (
|
) : (
|
||||||
<KeepSVG width={24} height={24} aria-hidden="true" />
|
<KeepSVG width={24} height={24} />
|
||||||
),
|
),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
if (doc.is_favorite) {
|
if (doc.is_favorite) {
|
||||||
@@ -88,7 +88,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('Share'),
|
label: t('Share'),
|
||||||
icon: <GroupSVG width={24} height={24} aria-hidden="true" />,
|
icon: <GroupSVG width={24} height={24} />,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
shareModal.open();
|
shareModal.open();
|
||||||
},
|
},
|
||||||
@@ -97,7 +97,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('Move into a doc'),
|
label: t('Move into a doc'),
|
||||||
icon: <DocMoveInSVG width={24} height={24} aria-hidden="true" />,
|
icon: <DocMoveInSVG width={24} height={24} />,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
importModal.open();
|
importModal.open();
|
||||||
},
|
},
|
||||||
@@ -106,7 +106,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('Duplicate'),
|
label: t('Duplicate'),
|
||||||
icon: <ContentCopySVG width={24} height={24} aria-hidden="true" />,
|
icon: <ContentCopySVG width={24} height={24} />,
|
||||||
disabled: !doc.abilities.duplicate,
|
disabled: !doc.abilities.duplicate,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
duplicateDoc({
|
duplicateDoc({
|
||||||
@@ -119,7 +119,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('Delete'),
|
label: t('Delete'),
|
||||||
icon: <DeleteSVG width={24} height={24} aria-hidden="true" />,
|
icon: <DeleteSVG width={24} height={24} />,
|
||||||
callback: () => deleteModal.open(),
|
callback: () => deleteModal.open(),
|
||||||
disabled: !doc.abilities.destroy,
|
disabled: !doc.abilities.destroy,
|
||||||
testId: `docs-grid-actions-remove-${doc.id}`,
|
testId: `docs-grid-actions-remove-${doc.id}`,
|
||||||
|
|||||||
+15
-13
@@ -1769,11 +1769,18 @@
|
|||||||
minimatch "^10.2.4"
|
minimatch "^10.2.4"
|
||||||
|
|
||||||
"@eslint/config-helpers@^0.5.2":
|
"@eslint/config-helpers@^0.5.2":
|
||||||
version "0.5.3"
|
version "0.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.5.3.tgz#721fe6bbb90d74b0c80d6ff2428e5bbcb002becb"
|
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.5.2.tgz#314c7b03d02a371ad8c0a7f6821d5a8a8437ba9d"
|
||||||
integrity sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==
|
integrity sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint/core" "^1.1.1"
|
"@eslint/core" "^1.1.0"
|
||||||
|
|
||||||
|
"@eslint/core@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-1.1.0.tgz#51f5cd970e216fbdae6721ac84491f57f965836d"
|
||||||
|
integrity sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==
|
||||||
|
dependencies:
|
||||||
|
"@types/json-schema" "^7.0.15"
|
||||||
|
|
||||||
"@eslint/core@^1.1.1":
|
"@eslint/core@^1.1.1":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
@@ -10344,9 +10351,9 @@ eslint@10.0.3:
|
|||||||
optionator "^0.9.3"
|
optionator "^0.9.3"
|
||||||
|
|
||||||
espree@^11.1.1:
|
espree@^11.1.1:
|
||||||
version "11.2.0"
|
version "11.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/espree/-/espree-11.2.0.tgz#01d5e47dc332aaba3059008362454a8cc34ccaa5"
|
resolved "https://registry.yarnpkg.com/espree/-/espree-11.1.1.tgz#866f6bc9ccccd6f28876b7a6463abb281b9cb847"
|
||||||
integrity sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==
|
integrity sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn "^8.16.0"
|
acorn "^8.16.0"
|
||||||
acorn-jsx "^5.3.2"
|
acorn-jsx "^5.3.2"
|
||||||
@@ -10722,12 +10729,7 @@ flat-cache@^6.1.20:
|
|||||||
flatted "^3.3.3"
|
flatted "^3.3.3"
|
||||||
hookified "^1.15.0"
|
hookified "^1.15.0"
|
||||||
|
|
||||||
flatted@^3.2.9:
|
flatted@^3.2.9, flatted@^3.3.3:
|
||||||
version "3.4.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726"
|
|
||||||
integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==
|
|
||||||
|
|
||||||
flatted@^3.3.3:
|
|
||||||
version "3.3.3"
|
version "3.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
|
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
|
||||||
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
|
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
|
||||||
|
|||||||
Reference in New Issue
Block a user