Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f2e07f949 | |||
| 8c1e95c587 | |||
| 20161fd6db | |||
| e827cfeee1 | |||
| eab2a75bff | |||
| cd84751cb9 | |||
| 1d20a8b0a7 | |||
| 8a310d004b | |||
| 9f9fae96e5 | |||
| 9cb2b6a6fb |
+9
-1
@@ -8,14 +8,22 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
### Added
|
||||
|
||||
- ✨(api) add API route to fetch document content #1206
|
||||
- ♿(frontend) improve accessibility:
|
||||
- #1349
|
||||
- #1271
|
||||
- #1341
|
||||
|
||||
### Changed
|
||||
|
||||
- 🔒️(backend) configure throttle on every viewsets #1343
|
||||
- ⬆️ Bump eslint to V9 #1071
|
||||
- ♿(frontend) improve accessibility:
|
||||
- ♿(frontend) fix major accessibility issues reported by wave and axe #1344
|
||||
- #1341
|
||||
- ♻️(tilt) use helm dev-backend chart
|
||||
|
||||
## [3.6.0] - 2025-09-04
|
||||
|
||||
|
||||
+3
-2
@@ -39,9 +39,10 @@ docker_build(
|
||||
]
|
||||
)
|
||||
|
||||
k8s_resource('impress-docs-backend-migrate', resource_deps=['postgres-postgresql'])
|
||||
k8s_resource('impress-docs-backend-migrate', resource_deps=['dev-backend-postgres'])
|
||||
k8s_resource('impress-docs-backend-createsuperuser', resource_deps=['impress-docs-backend-migrate'])
|
||||
k8s_resource('impress-docs-backend', resource_deps=['impress-docs-backend-migrate'])
|
||||
k8s_resource('dev-backend-keycloak', resource_deps=['dev-backend-keycloak-pg'])
|
||||
k8s_resource('impress-docs-backend', resource_deps=['impress-docs-backend-migrate', 'dev-backend-redis', 'dev-backend-keycloak', 'dev-backend-postgres', 'dev-backend-minio:statefulset'])
|
||||
k8s_yaml(local('cd ../src/helm && helmfile -n impress -e dev template .'))
|
||||
|
||||
migration = '''
|
||||
|
||||
@@ -128,3 +128,11 @@ class ListDocumentFilter(DocumentFilter):
|
||||
|
||||
queryset_method = queryset.filter if bool(value) else queryset.exclude
|
||||
return queryset_method(link_traces__user=user, link_traces__is_masked=True)
|
||||
|
||||
|
||||
class UserSearchFilter(django_filters.FilterSet):
|
||||
"""
|
||||
Custom filter for searching users.
|
||||
"""
|
||||
|
||||
q = django_filters.CharFilter(min_length=5, max_length=254)
|
||||
|
||||
@@ -51,7 +51,7 @@ from core.tasks.mail import send_ask_for_access_mail
|
||||
from core.utils import extract_attachments, filter_descendants
|
||||
|
||||
from . import permissions, serializers, utils
|
||||
from .filters import DocumentFilter, ListDocumentFilter
|
||||
from .filters import DocumentFilter, ListDocumentFilter, UserSearchFilter
|
||||
from .throttling import UserListThrottleBurst, UserListThrottleSustained
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -176,12 +176,18 @@ class UserViewSet(
|
||||
if self.action != "list":
|
||||
return queryset
|
||||
|
||||
filterset = UserSearchFilter(
|
||||
self.request.GET, queryset=queryset, request=self.request
|
||||
)
|
||||
if not filterset.is_valid():
|
||||
raise drf.exceptions.ValidationError(filterset.errors)
|
||||
|
||||
# Exclude all users already in the given document
|
||||
if document_id := self.request.query_params.get("document_id", ""):
|
||||
queryset = queryset.exclude(documentaccess__document_id=document_id)
|
||||
|
||||
if not (query := self.request.query_params.get("q", "")) or len(query) < 5:
|
||||
return queryset.none()
|
||||
filter_data = filterset.form.cleaned_data
|
||||
query = filter_data["q"]
|
||||
|
||||
# For emails, match emails by Levenstein distance to prevent typing errors
|
||||
if "@" in query:
|
||||
|
||||
@@ -194,18 +194,41 @@ def test_api_users_list_query_short_queries():
|
||||
factories.UserFactory(email="john.lennon@example.com")
|
||||
|
||||
response = client.get("/api/v1.0/users/?q=jo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"q": ["Ensure this value has at least 5 characters (it has 2)."]
|
||||
}
|
||||
|
||||
response = client.get("/api/v1.0/users/?q=john")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"q": ["Ensure this value has at least 5 characters (it has 4)."]
|
||||
}
|
||||
|
||||
response = client.get("/api/v1.0/users/?q=john.")
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) == 2
|
||||
|
||||
|
||||
def test_api_users_list_query_long_queries():
|
||||
"""
|
||||
Queries longer than 255 characters should return an empty result set.
|
||||
"""
|
||||
user = factories.UserFactory(email="paul@example.com")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
factories.UserFactory(email="john.doe@example.com")
|
||||
factories.UserFactory(email="john.lennon@example.com")
|
||||
|
||||
query = "a" * 244
|
||||
response = client.get(f"/api/v1.0/users/?q={query}@example.com")
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"q": ["Ensure this value has at most 254 characters (it has 256)."]
|
||||
}
|
||||
|
||||
|
||||
def test_api_users_list_query_inactive():
|
||||
"""Inactive users should not be listed."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -8,11 +8,19 @@ NB_OBJECTS = {
|
||||
|
||||
DEV_USERS = [
|
||||
{"username": "impress", "email": "impress@impress.world", "language": "en-us"},
|
||||
{"username": "user-e2e-webkit", "email": "user@webkit.test", "language": "en-us"},
|
||||
{"username": "user-e2e-firefox", "email": "user@firefox.test", "language": "en-us"},
|
||||
{
|
||||
"username": "user-e2e-webkit",
|
||||
"email": "user.test@webkit.test",
|
||||
"language": "en-us",
|
||||
},
|
||||
{
|
||||
"username": "user-e2e-firefox",
|
||||
"email": "user.test@firefox.test",
|
||||
"language": "en-us",
|
||||
},
|
||||
{
|
||||
"username": "user-e2e-chromium",
|
||||
"email": "user@chromium.test",
|
||||
"email": "user.test@chromium.test",
|
||||
"language": "en-us",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -119,8 +119,8 @@ def create_demo(stdout):
|
||||
first_name = random.choice(first_names)
|
||||
queue.push(
|
||||
models.User(
|
||||
admin_email=f"user{i:d}@example.com",
|
||||
email=f"user{i:d}@example.com",
|
||||
admin_email=f"user.test{i:d}@example.com",
|
||||
email=f"user.test{i:d}@example.com",
|
||||
password="!",
|
||||
is_superuser=False,
|
||||
is_active=True,
|
||||
|
||||
@@ -33,9 +33,9 @@ def test_commands_create_demo():
|
||||
# assert dev users have doc accesses
|
||||
user = models.User.objects.get(email="impress@impress.world")
|
||||
assert models.DocumentAccess.objects.filter(user=user).exists()
|
||||
user = models.User.objects.get(email="user@webkit.test")
|
||||
user = models.User.objects.get(email="user.test@webkit.test")
|
||||
assert models.DocumentAccess.objects.filter(user=user).exists()
|
||||
user = models.User.objects.get(email="user@firefox.test")
|
||||
user = models.User.objects.get(email="user.test@firefox.test")
|
||||
assert models.DocumentAccess.objects.filter(user=user).exists()
|
||||
user = models.User.objects.get(email="user@chromium.test")
|
||||
user = models.User.objects.get(email="user.test@chromium.test")
|
||||
assert models.DocumentAccess.objects.filter(user=user).exists()
|
||||
|
||||
@@ -39,7 +39,7 @@ dependencies = [
|
||||
"django-redis==6.0.0",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django==5.2.4",
|
||||
"django==5.2.6",
|
||||
"django-treebeard==4.7.1",
|
||||
"djangorestframework==3.16.0",
|
||||
"drf_spectacular==0.28.0",
|
||||
|
||||
@@ -45,8 +45,8 @@ test.describe('Doc Create', () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
const input = page.getByRole('textbox', { name: 'doc title input' });
|
||||
await expect(input).toHaveText('');
|
||||
const input = page.getByRole('textbox', { name: 'Document title' });
|
||||
await expect(input).toHaveText('', { timeout: 10000 });
|
||||
await expect(
|
||||
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
|
||||
).toBeVisible();
|
||||
@@ -67,8 +67,8 @@ test.describe('Doc Create', () => {
|
||||
.getByText('New sub-doc')
|
||||
.click();
|
||||
|
||||
const input = page.getByRole('textbox', { name: 'doc title input' });
|
||||
await expect(input).toHaveText('');
|
||||
const input = page.getByRole('textbox', { name: 'Document title' });
|
||||
await expect(input).toHaveText('', { timeout: 10000 });
|
||||
await expect(
|
||||
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -226,9 +226,13 @@ test.describe('Doc Editor', () => {
|
||||
await editor.fill('Hello World Doc persisted 2');
|
||||
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const urlDoc = page.url();
|
||||
await page.goto(urlDoc);
|
||||
|
||||
// Wait for editor to load
|
||||
await expect(editor).toBeVisible();
|
||||
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -463,12 +467,14 @@ test.describe('Doc Editor', () => {
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Download',
|
||||
exact: true,
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
void page
|
||||
.getByRole('button', {
|
||||
name: 'Download',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
|
||||
@@ -38,9 +38,11 @@ test.describe('Doc Export', () => {
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('combobox', { name: 'Format' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Close the modal' }),
|
||||
page.getByRole('button', {
|
||||
name: 'Close the download modal',
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId('modal-download-button')).toBeVisible();
|
||||
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it exports the doc with pdf line break', async ({
|
||||
@@ -81,12 +83,7 @@ test.describe('Doc Export', () => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
});
|
||||
|
||||
void page
|
||||
.getByRole('button', {
|
||||
name: 'Download',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
@@ -131,13 +128,13 @@ test.describe('Doc Export', () => {
|
||||
await page.getByRole('combobox', { name: 'Format' }).click();
|
||||
await page.getByRole('option', { name: 'Docx' }).click();
|
||||
|
||||
await expect(page.getByTestId('modal-download-button')).toBeVisible();
|
||||
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.docx`);
|
||||
});
|
||||
|
||||
void page.getByTestId('modal-download-button').click();
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.docx`);
|
||||
@@ -203,7 +200,7 @@ test.describe('Doc Export', () => {
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
await expect(page.getByTestId('modal-download-button')).toBeVisible();
|
||||
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
|
||||
|
||||
const responseCorsPromise = page.waitForResponse(
|
||||
(response) =>
|
||||
@@ -214,7 +211,7 @@ test.describe('Doc Export', () => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
});
|
||||
|
||||
void page.getByTestId('modal-download-button').click();
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const responseCors = await responseCorsPromise;
|
||||
expect(responseCors.ok()).toBe(true);
|
||||
@@ -256,13 +253,13 @@ test.describe('Doc Export', () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(page.getByTestId('modal-download-button')).toBeVisible();
|
||||
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
});
|
||||
|
||||
void page.getByTestId('modal-download-button').click();
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
@@ -298,13 +295,15 @@ test.describe('Doc Export', () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(page.getByTestId('modal-download-button')).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId('doc-open-modal-download-button'),
|
||||
).toBeVisible();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
});
|
||||
|
||||
void page.getByTestId('modal-download-button').click();
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
@@ -350,13 +349,15 @@ test.describe('Doc Export', () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(page.getByTestId('modal-download-button')).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId('doc-open-modal-download-button'),
|
||||
).toBeVisible();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
});
|
||||
|
||||
void page.getByTestId('modal-download-button').click();
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
@@ -392,14 +393,9 @@ test.describe('Doc Export', () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.waitForURL('**/docs/**', {
|
||||
timeout: 10000,
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
const input = page.getByLabel('doc title input');
|
||||
const input = page.locator('.--docs--doc-title-input[role="textbox"]');
|
||||
await expect(input).toBeVisible();
|
||||
await expect(input).toHaveText('');
|
||||
await expect(input).toHaveText('', { timeout: 10000 });
|
||||
await input.click();
|
||||
await input.fill(randomDocFrench);
|
||||
await input.blur();
|
||||
@@ -418,7 +414,7 @@ test.describe('Doc Export', () => {
|
||||
return download.suggestedFilename().includes(`${randomDocFrench}.pdf`);
|
||||
});
|
||||
|
||||
void page.getByTestId('modal-download-button').click();
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDocFrench}.pdf`);
|
||||
@@ -453,19 +449,23 @@ test.describe('Doc Export', () => {
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Link a doc').first().click();
|
||||
|
||||
await page
|
||||
.locator(
|
||||
"span[data-inline-content-type='interlinkingSearchInline'] input",
|
||||
)
|
||||
.fill('interlink-child');
|
||||
const input = page.locator(
|
||||
"span[data-inline-content-type='interlinkingSearchInline'] input",
|
||||
);
|
||||
const searchContainer = page.locator('.quick-search-container');
|
||||
|
||||
await page
|
||||
.locator('.quick-search-container')
|
||||
.getByText('interlink-child')
|
||||
.click();
|
||||
await input.fill('export-interlink');
|
||||
|
||||
const interlink = page.getByRole('link', {
|
||||
name: 'interlink-child',
|
||||
await expect(searchContainer).toBeVisible();
|
||||
await expect(searchContainer.getByText(randomDoc)).toBeVisible();
|
||||
|
||||
// We are in docChild, we want to create a link to randomDoc (parent)
|
||||
await searchContainer.getByText(randomDoc).click();
|
||||
|
||||
// Search the interlinking link in the editor (not in the document tree)
|
||||
const editor = page.locator('.ProseMirror.bn-editor');
|
||||
const interlink = editor.getByRole('link', {
|
||||
name: randomDoc,
|
||||
});
|
||||
|
||||
await expect(interlink).toBeVisible();
|
||||
@@ -480,7 +480,7 @@ test.describe('Doc Export', () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
void page.getByTestId('modal-download-button').click();
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${docChild}.pdf`);
|
||||
@@ -488,6 +488,6 @@ test.describe('Doc Export', () => {
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfData = await pdf(pdfBuffer);
|
||||
|
||||
expect(pdfData.text).toContain('interlink-child'); // This is the pdf text
|
||||
expect(pdfData.text).toContain(randomDoc);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,9 +80,7 @@ test.describe('Documents Grid mobile', () => {
|
||||
hasText: 'My mocked document',
|
||||
});
|
||||
|
||||
await expect(
|
||||
row.locator('[aria-describedby="doc-title"]').nth(0),
|
||||
).toHaveText('My mocked document');
|
||||
await expect(row.getByTestId('doc-title')).toHaveText('My mocked document');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -149,7 +147,7 @@ test.describe('Document grid item options', () => {
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Confirm deletion',
|
||||
name: 'Delete document',
|
||||
})
|
||||
.click();
|
||||
|
||||
@@ -295,7 +293,7 @@ test.describe('Documents Grid', () => {
|
||||
docs = result.results as SmallDoc[];
|
||||
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
await expect(page.locator('h4').getByText('All docs')).toBeVisible();
|
||||
await expect(page.locator('h2').getByText('All docs')).toBeVisible();
|
||||
|
||||
const thead = page.getByTestId('docs-grid-header');
|
||||
await expect(thead.getByText(/Name/i)).toBeVisible();
|
||||
|
||||
@@ -25,7 +25,7 @@ test.describe('Doc Header', () => {
|
||||
'It is the card information about the document.',
|
||||
);
|
||||
|
||||
const docTitle = card.getByRole('textbox', { name: 'doc title input' });
|
||||
const docTitle = card.getByRole('textbox', { name: 'Document title' });
|
||||
await expect(docTitle).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
@@ -54,7 +54,7 @@ test.describe('Doc Header', () => {
|
||||
|
||||
test('it updates the title doc', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'doc-update', browserName, 1);
|
||||
const docTitle = page.getByRole('textbox', { name: 'doc title input' });
|
||||
const docTitle = page.getByRole('textbox', { name: 'Document title' });
|
||||
await expect(docTitle).toBeVisible();
|
||||
await docTitle.fill('Hello World');
|
||||
await docTitle.blur();
|
||||
@@ -66,7 +66,7 @@ test.describe('Doc Header', () => {
|
||||
browserName,
|
||||
}) => {
|
||||
await createDoc(page, 'doc-update', browserName, 1);
|
||||
const docTitle = page.getByRole('textbox', { name: 'doc title input' });
|
||||
const docTitle = page.getByRole('textbox', { name: 'Document title' });
|
||||
await expect(docTitle).toBeVisible();
|
||||
await docTitle.fill('👍 Hello Emoji World');
|
||||
await docTitle.blur();
|
||||
@@ -100,7 +100,7 @@ test.describe('Doc Header', () => {
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Confirm deletion',
|
||||
name: 'Delete document',
|
||||
})
|
||||
.click();
|
||||
|
||||
@@ -228,23 +228,27 @@ test.describe('Doc Header', () => {
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
const shareModal = page.getByLabel('Share modal');
|
||||
const shareModal = page.getByRole('dialog', {
|
||||
name: 'Share modal content',
|
||||
});
|
||||
await expect(shareModal).toBeVisible();
|
||||
await expect(page.getByText('Share the document')).toBeVisible();
|
||||
|
||||
await expect(page.getByPlaceholder('Type a name or email')).toBeHidden();
|
||||
|
||||
const invitationCard = shareModal.getByLabel('List invitation card');
|
||||
await expect(invitationCard).toBeVisible();
|
||||
await expect(
|
||||
invitationCard.getByText('test@invitation.test').first(),
|
||||
).toBeVisible();
|
||||
await expect(invitationCard.getByLabel('doc-role-text')).toBeVisible();
|
||||
await expect(invitationCard.getByLabel('Document role text')).toBeVisible();
|
||||
await expect(
|
||||
invitationCard.getByRole('button', { name: 'more_horiz' }),
|
||||
).toBeHidden();
|
||||
|
||||
const memberCard = shareModal.getByLabel('List members card');
|
||||
await expect(memberCard.getByText('test@accesses.test')).toBeVisible();
|
||||
await expect(memberCard.getByLabel('doc-role-text')).toBeVisible();
|
||||
await expect(memberCard.getByLabel('Document role text')).toBeVisible();
|
||||
await expect(
|
||||
memberCard.getByRole('button', { name: 'more_horiz' }),
|
||||
).toBeHidden();
|
||||
@@ -296,17 +300,18 @@ test.describe('Doc Header', () => {
|
||||
await expect(page.getByPlaceholder('Type a name or email')).toBeHidden();
|
||||
|
||||
const invitationCard = shareModal.getByLabel('List invitation card');
|
||||
await expect(invitationCard).toBeVisible();
|
||||
await expect(
|
||||
invitationCard.getByText('test@invitation.test').first(),
|
||||
).toBeVisible();
|
||||
await expect(invitationCard.getByLabel('doc-role-text')).toBeVisible();
|
||||
await expect(invitationCard.getByLabel('Document role text')).toBeVisible();
|
||||
await expect(
|
||||
invitationCard.getByRole('button', { name: 'more_horiz' }),
|
||||
).toBeHidden();
|
||||
|
||||
const memberCard = shareModal.getByLabel('List members card');
|
||||
await expect(memberCard.getByText('test@accesses.test')).toBeVisible();
|
||||
await expect(memberCard.getByLabel('doc-role-text')).toBeVisible();
|
||||
await expect(memberCard.getByLabel('Document role text')).toBeVisible();
|
||||
await expect(
|
||||
memberCard.getByRole('button', { name: 'more_horiz' }),
|
||||
).toBeHidden();
|
||||
|
||||
@@ -15,7 +15,7 @@ test.describe('Document create member', () => {
|
||||
});
|
||||
|
||||
test('it selects 2 users and 1 invitation', async ({ page, browserName }) => {
|
||||
const inputFill = 'user ';
|
||||
const inputFill = 'user.test';
|
||||
const responsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/users/?q=${encodeURIComponent(inputFill)}`) &&
|
||||
|
||||
@@ -60,32 +60,37 @@ test.describe('Doc Routing', () => {
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
|
||||
const responsePromise = page.route(
|
||||
/.*\/documents\/.*\/$|users\/me\/$/,
|
||||
async (route) => {
|
||||
const request = route.request();
|
||||
// Wait for the doc link (via its dynamic title) to be visible
|
||||
const docLink = page.getByRole('link', { name: docTitle });
|
||||
await expect(docLink).toBeVisible();
|
||||
|
||||
if (
|
||||
request.method().includes('PATCH') ||
|
||||
request.method().includes('GET')
|
||||
) {
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
json: {
|
||||
detail: 'Log in to access the document',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
},
|
||||
// Intercept GET/PATCH requests to return 401
|
||||
await page.route(/.*\/documents\/.*\/$|users\/me\/$/, async (route) => {
|
||||
const request = route.request();
|
||||
if (
|
||||
request.method().includes('PATCH') ||
|
||||
request.method().includes('GET')
|
||||
) {
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
json: { detail: 'Log in to access the document' },
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
// Explicitly wait for a 401 response after clicking
|
||||
const wait401 = page.waitForResponse(
|
||||
(resp) =>
|
||||
resp.status() === 401 &&
|
||||
/\/(documents\/[^/]+\/|users\/me\/)$/.test(resp.url()),
|
||||
);
|
||||
|
||||
await page.getByRole('link', { name: '401-doc-parent' }).click();
|
||||
await docLink.click();
|
||||
await wait401;
|
||||
|
||||
await responsePromise;
|
||||
|
||||
await expect(page.getByText('Log in to access the document')).toBeVisible({
|
||||
await expect(page.getByText('Log in to access the document.')).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ test.describe('Document search', () => {
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByLabel('Search modal').getByText('search'),
|
||||
page.getByRole('heading', { name: 'Search docs' }),
|
||||
).toBeVisible();
|
||||
|
||||
const inputSearch = page.getByPlaceholder('Type the name of a document');
|
||||
@@ -79,7 +79,7 @@ test.describe('Document search', () => {
|
||||
|
||||
await page.keyboard.press('Control+k');
|
||||
await expect(
|
||||
page.getByLabel('Search modal').getByText('search'),
|
||||
page.getByRole('heading', { name: 'Search docs' }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
@@ -173,12 +173,13 @@ test.describe('Document search', () => {
|
||||
.getByRole('combobox', { name: 'Quick search input' })
|
||||
.fill('sub page search');
|
||||
|
||||
// Expect to find the first doc
|
||||
// Expect to find the first and second docs in the results list
|
||||
const resultsList = page.getByRole('listbox');
|
||||
await expect(
|
||||
page.getByRole('presentation').getByLabel(firstDocTitle),
|
||||
resultsList.getByRole('option', { name: firstDocTitle }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('presentation').getByLabel(secondDocTitle),
|
||||
resultsList.getByRole('option', { name: secondDocTitle }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'close' }).click();
|
||||
@@ -195,14 +196,15 @@ test.describe('Document search', () => {
|
||||
.fill('second');
|
||||
|
||||
// Now there is a sub page - expect to have the focus on the current doc
|
||||
const updatedResultsList = page.getByRole('listbox');
|
||||
await expect(
|
||||
page.getByRole('presentation').getByLabel(secondDocTitle),
|
||||
updatedResultsList.getByRole('option', { name: secondDocTitle }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('presentation').getByLabel(secondChildDocTitle),
|
||||
updatedResultsList.getByRole('option', { name: secondChildDocTitle }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('presentation').getByLabel(firstDocTitle),
|
||||
updatedResultsList.getByRole('option', { name: firstDocTitle }),
|
||||
).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ test.describe('Doc Tree', () => {
|
||||
await expect(subPageItem).toBeVisible();
|
||||
await subPageItem.click();
|
||||
await verifyDocName(page, '');
|
||||
const input = page.getByRole('textbox', { name: 'doc title input' });
|
||||
const input = page.getByRole('textbox', { name: 'Document title' });
|
||||
await input.click();
|
||||
const [randomDocName] = randomName('doc-tree-test', browserName, 1);
|
||||
await input.fill(randomDocName);
|
||||
@@ -196,7 +196,7 @@ test.describe('Doc Tree', () => {
|
||||
await page.getByText('Move to my docs').click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'doc title input' }),
|
||||
page.getByRole('textbox', { name: 'Document title' }),
|
||||
).not.toHaveText(docChild);
|
||||
|
||||
const header = page.locator('header').first();
|
||||
|
||||
@@ -101,10 +101,9 @@ export const createDoc = async (
|
||||
waitUntil: 'networkidle',
|
||||
});
|
||||
|
||||
const input = page.getByLabel('doc title input');
|
||||
const input = page.getByLabel('Document title');
|
||||
await expect(input).toBeVisible();
|
||||
await expect(input).toHaveText('');
|
||||
await input.click();
|
||||
|
||||
await input.fill(randomDocs[i]);
|
||||
await input.blur();
|
||||
@@ -120,10 +119,11 @@ export const verifyDocName = async (page: Page, docName: string) => {
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
/*replace toHaveText with toContainText to handle cases where emojis or other characters might be added*/
|
||||
try {
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'doc title input' }),
|
||||
).toHaveText(docName);
|
||||
page.getByRole('textbox', { name: 'Document title' }),
|
||||
).toContainText(docName);
|
||||
} catch {
|
||||
await expect(page.getByRole('heading', { name: docName })).toBeVisible();
|
||||
}
|
||||
@@ -172,7 +172,7 @@ export const goToGridDoc = async (
|
||||
|
||||
await expect(row).toBeVisible();
|
||||
|
||||
const docTitleContent = row.locator('[aria-describedby="doc-title"]').first();
|
||||
const docTitleContent = row.getByTestId('doc-title').first();
|
||||
const docTitle = await docTitleContent.textContent();
|
||||
expect(docTitle).toBeDefined();
|
||||
|
||||
@@ -182,9 +182,9 @@ export const goToGridDoc = async (
|
||||
};
|
||||
|
||||
export const updateDocTitle = async (page: Page, title: string) => {
|
||||
const input = page.getByLabel('doc title input');
|
||||
await expect(input).toBeVisible();
|
||||
const input = page.getByRole('textbox', { name: 'Document title' });
|
||||
await expect(input).toHaveText('');
|
||||
await expect(input).toBeVisible();
|
||||
await input.click();
|
||||
await input.fill(title);
|
||||
await input.click();
|
||||
|
||||
@@ -8,7 +8,7 @@ export const addNewMember = async (
|
||||
page: Page,
|
||||
index: number,
|
||||
role: 'Administrator' | 'Owner' | 'Editor' | 'Reader',
|
||||
fillText: string = 'user ',
|
||||
fillText: string = 'user.test',
|
||||
) => {
|
||||
const responsePromiseSearchUser = page.waitForResponse(
|
||||
(response) =>
|
||||
|
||||
@@ -28,17 +28,18 @@ const StyledButton = styled(Button)<StyledButtonProps>`
|
||||
border: none;
|
||||
background: none;
|
||||
outline: none;
|
||||
transition: all 0.2s ease-in-out;
|
||||
font-weight: 500;
|
||||
font-size: 0.938rem;
|
||||
padding: 0;
|
||||
${({ $css }) => $css};
|
||||
|
||||
&:hover {
|
||||
background-color: var(
|
||||
--c--components--button--primary-text--background--color-hover
|
||||
);
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--c--theme--colors--primary-500);
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 2px var(--c--theme--colors--primary-400);
|
||||
border-radius: 4px;
|
||||
transition: none;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -110,7 +110,6 @@ export const DropdownMenu = ({
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$position="relative"
|
||||
aria-controls="menu"
|
||||
>
|
||||
<Box>{children}</Box>
|
||||
<Icon
|
||||
@@ -125,9 +124,7 @@ export const DropdownMenu = ({
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box ref={blockButtonRef} aria-controls="menu">
|
||||
{children}
|
||||
</Box>
|
||||
<Box ref={blockButtonRef}>{children}</Box>
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -207,14 +204,13 @@ export const DropdownMenu = ({
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--c--theme--colors--primary-500);
|
||||
outline: 2px solid var(--c--theme--colors--primary-400);
|
||||
outline-offset: -2px;
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
}
|
||||
|
||||
${isFocused &&
|
||||
css`
|
||||
outline: 2px solid var(--c--theme--colors--primary-500);
|
||||
outline-offset: -2px;
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
`}
|
||||
|
||||
@@ -30,15 +30,23 @@ export const AlertModal = ({
|
||||
isOpen={isOpen}
|
||||
size={ModalSize.MEDIUM}
|
||||
onClose={onClose}
|
||||
aria-describedby="alert-modal-title"
|
||||
title={
|
||||
<Text $size="h6" $align="flex-start" $variation="1000">
|
||||
<Text
|
||||
$size="h6"
|
||||
as="h1"
|
||||
$margin="0"
|
||||
id="alert-modal-title"
|
||||
$align="flex-start"
|
||||
$variation="1000"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
}
|
||||
rightActions={
|
||||
<>
|
||||
<Button
|
||||
aria-label={t('Close the modal')}
|
||||
aria-label={`${t('Cancel')} - ${title}`}
|
||||
color="secondary"
|
||||
fullWidth
|
||||
onClick={() => onClose()}
|
||||
@@ -55,12 +63,11 @@ export const AlertModal = ({
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
aria-label={t('Confirmation button')}
|
||||
className="--docs--alert-modal"
|
||||
>
|
||||
<Box className="--docs--alert-modal">
|
||||
<Box>
|
||||
<Text $variation="600">{description}</Text>
|
||||
<Text $variation="600" as="p">
|
||||
{description}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
|
||||
@@ -64,6 +64,7 @@ export const QuickSearchInput = ({
|
||||
role="combobox"
|
||||
placeholder={placeholder ?? t('Search')}
|
||||
onValueChange={onFilter}
|
||||
maxLength={254}
|
||||
/>
|
||||
</Box>
|
||||
{separator && <HorizontalSeparator $withPadding={false} />}
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Button } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { BoxButton } from '@/components';
|
||||
import { Box, BoxButton } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import ProConnectImg from '../assets/button-proconnect.svg';
|
||||
import { useAuth } from '../hooks';
|
||||
@@ -11,6 +12,7 @@ import { gotoLogin, gotoLogout } from '../utils';
|
||||
export const ButtonLogin = () => {
|
||||
const { t } = useTranslation();
|
||||
const { authenticated } = useAuth();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
if (!authenticated) {
|
||||
return (
|
||||
@@ -26,14 +28,23 @@ export const ButtonLogin = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={gotoLogout}
|
||||
color="primary-text"
|
||||
aria-label={t('Logout')}
|
||||
className="--docs--button-logout"
|
||||
<Box
|
||||
$css={css`
|
||||
.--docs--button-logout:focus-visible {
|
||||
box-shadow: 0 0 0 2px ${colorsTokens['primary-400']} !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{t('Logout')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={gotoLogout}
|
||||
color="primary-text"
|
||||
aria-label={t('Logout')}
|
||||
className="--docs--button-logout"
|
||||
>
|
||||
{t('Logout')}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+6
-1
@@ -19,10 +19,11 @@ export const ModalConfirmDownloadUnsafe = ({
|
||||
isOpen
|
||||
closeOnClickOutside
|
||||
onClose={() => onClose()}
|
||||
aria-describedby="modal-confirm-download-unsafe-title"
|
||||
rightActions={
|
||||
<>
|
||||
<Button
|
||||
aria-label={t('Close the modal')}
|
||||
aria-label={t('Cancel the download')}
|
||||
color="secondary"
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
@@ -31,6 +32,7 @@ export const ModalConfirmDownloadUnsafe = ({
|
||||
<Button
|
||||
aria-label={t('Download')}
|
||||
color="danger"
|
||||
data-testid="modal-download-unsafe-button"
|
||||
onClick={() => {
|
||||
if (onConfirm) {
|
||||
void onConfirm();
|
||||
@@ -45,11 +47,14 @@ export const ModalConfirmDownloadUnsafe = ({
|
||||
size={ModalSize.SMALL}
|
||||
title={
|
||||
<Text
|
||||
as="h1"
|
||||
id="modal-confirm-download-unsafe-title"
|
||||
$gap="0.7rem"
|
||||
$size="h6"
|
||||
$align="flex-start"
|
||||
$variation="1000"
|
||||
$direction="row"
|
||||
$margin="0"
|
||||
>
|
||||
<Icon iconName="warning" $theme="warning" />
|
||||
{t('Warning')}
|
||||
|
||||
@@ -133,10 +133,11 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
closeOnClickOutside
|
||||
onClose={() => onClose()}
|
||||
hideCloseButton
|
||||
aria-describedby="modal-export-title"
|
||||
rightActions={
|
||||
<>
|
||||
<Button
|
||||
aria-label={t('Close the modal')}
|
||||
aria-label={t('Cancel the download')}
|
||||
color="secondary"
|
||||
fullWidth
|
||||
onClick={() => onClose()}
|
||||
@@ -145,12 +146,12 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="doc-export-download-button"
|
||||
aria-label={t('Download')}
|
||||
color="primary"
|
||||
fullWidth
|
||||
onClick={() => void onSubmit()}
|
||||
disabled={isExporting}
|
||||
data-testid="modal-download-button"
|
||||
>
|
||||
{t('Download')}
|
||||
</Button>
|
||||
@@ -165,6 +166,9 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
$width="100%"
|
||||
>
|
||||
<Text
|
||||
as="h1"
|
||||
$margin="0"
|
||||
id="modal-export-title"
|
||||
$size="h6"
|
||||
$variation="1000"
|
||||
$align="flex-start"
|
||||
@@ -186,7 +190,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
$gap="1rem"
|
||||
className="--docs--modal-export-content"
|
||||
>
|
||||
<Text $variation="600" $size="sm">
|
||||
<Text $variation="600" $size="sm" as="p">
|
||||
{t('Download your document in a .docx or .pdf format.')}
|
||||
</Text>
|
||||
<Select
|
||||
|
||||
@@ -105,7 +105,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
}, [doc]);
|
||||
|
||||
return (
|
||||
<Tooltip content={t('Rename')} placement="top">
|
||||
<Tooltip content={t('Rename')} aria-hidden={true} placement="top">
|
||||
<Box
|
||||
as="span"
|
||||
role="textbox"
|
||||
@@ -114,7 +114,8 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
defaultValue={titleDisplay || undefined}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
suppressContentEditableWarning={true}
|
||||
aria-label="doc title input"
|
||||
aria-label={`${t('Document title')}`}
|
||||
aria-multiline={false}
|
||||
onBlurCapture={(event) =>
|
||||
handleTitleSubmit(event.target.textContent || '')
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
>
|
||||
<Button
|
||||
color="tertiary"
|
||||
aria-label="Share button"
|
||||
aria-label={t('Share button')}
|
||||
icon={
|
||||
<Icon iconName="group" $theme="primary" $variation="800" />
|
||||
}
|
||||
@@ -233,9 +233,15 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
|
||||
{!isSmallMobile && ModalExport && (
|
||||
<Button
|
||||
data-testid="doc-open-modal-download-button"
|
||||
color="tertiary-text"
|
||||
icon={
|
||||
<Icon iconName="download" $theme="primary" $variation="800" />
|
||||
<Icon
|
||||
iconName="download"
|
||||
$theme="primary"
|
||||
$variation="800"
|
||||
aria-hidden={true}
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
setIsModalExportOpen(true);
|
||||
|
||||
+8
-9
@@ -56,10 +56,11 @@ export const ModalRemoveDoc = ({
|
||||
closeOnClickOutside
|
||||
hideCloseButton
|
||||
onClose={() => onClose()}
|
||||
aria-describedby="modal-remove-doc-title"
|
||||
rightActions={
|
||||
<>
|
||||
<Button
|
||||
aria-label={t('Close the delete modal')}
|
||||
aria-label={t('Cancel the deletion')}
|
||||
color="secondary"
|
||||
fullWidth
|
||||
onClick={() => onClose()}
|
||||
@@ -67,7 +68,7 @@ export const ModalRemoveDoc = ({
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Confirm deletion')}
|
||||
aria-label={t('Delete document')}
|
||||
color="danger"
|
||||
fullWidth
|
||||
onClick={() =>
|
||||
@@ -90,8 +91,9 @@ export const ModalRemoveDoc = ({
|
||||
>
|
||||
<Text
|
||||
$size="h6"
|
||||
as="h6"
|
||||
$margin={{ all: '0' }}
|
||||
as="h1"
|
||||
id="modal-remove-doc-title"
|
||||
$margin="0"
|
||||
$align="flex-start"
|
||||
$variation="1000"
|
||||
>
|
||||
@@ -104,12 +106,9 @@ export const ModalRemoveDoc = ({
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
aria-label={t('Content modal to delete document')}
|
||||
className="--docs--modal-remove-doc"
|
||||
>
|
||||
<Box className="--docs--modal-remove-doc">
|
||||
{!isError && (
|
||||
<Text $size="sm" $variation="600" $display="inline-block">
|
||||
<Text $size="sm" $variation="600" $display="inline-block" as="p">
|
||||
<Trans t={t}>
|
||||
This document and <strong>any sub-documents</strong> will be
|
||||
permanently deleted. This action is irreversible.
|
||||
|
||||
+1
-2
@@ -82,12 +82,11 @@ export const SimpleDocItem = ({
|
||||
</Box>
|
||||
<Box $justify="center" $overflow="auto">
|
||||
<Text
|
||||
aria-describedby="doc-title"
|
||||
aria-label={displayTitle}
|
||||
$size="sm"
|
||||
$variation="1000"
|
||||
$weight="500"
|
||||
$css={ItemTextCss}
|
||||
data-testid="doc-title"
|
||||
>
|
||||
{displayTitle}
|
||||
</Text>
|
||||
|
||||
+10
-1
@@ -5,7 +5,7 @@ import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { Box, Text } from '@/components';
|
||||
import ButtonCloseModal from '@/components/modal/ButtonCloseModal';
|
||||
import { QuickSearch } from '@/components/quick-search';
|
||||
import { Doc, useDocUtils } from '@/docs/doc-management';
|
||||
@@ -65,6 +65,7 @@ const DocSearchModalGlobal = ({
|
||||
closeOnClickOutside
|
||||
size={isDesktop ? ModalSize.LARGE : ModalSize.FULL}
|
||||
hideCloseButton
|
||||
aria-describedby="doc-search-modal-title"
|
||||
>
|
||||
<Box
|
||||
aria-label={t('Search modal')}
|
||||
@@ -72,6 +73,14 @@ const DocSearchModalGlobal = ({
|
||||
$justify="space-between"
|
||||
className="--docs--doc-search-modal"
|
||||
>
|
||||
<Text
|
||||
as="h1"
|
||||
$margin="0"
|
||||
id="doc-search-modal-title"
|
||||
className="sr-only"
|
||||
>
|
||||
{t('Search docs')}
|
||||
</Text>
|
||||
<Box $position="absolute" $css="top: 12px; right: 12px;">
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the search modal')}
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ export const DocRoleDropdown = ({
|
||||
|
||||
if (!canUpdate) {
|
||||
return (
|
||||
<Text aria-label="doc-role-text" $variation="600">
|
||||
<Text aria-label={t('Document role text')} $variation="600">
|
||||
{transRole(currentRole)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
@@ -135,12 +135,21 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
isOpen
|
||||
closeOnClickOutside
|
||||
data-testid="doc-share-modal"
|
||||
aria-label={t('Share modal')}
|
||||
aria-describedby="doc-share-modal-title"
|
||||
size={isDesktop ? ModalSize.LARGE : ModalSize.FULL}
|
||||
onClose={onClose}
|
||||
title={
|
||||
<Box $direction="row" $justify="space-between" $align="center">
|
||||
<Box $align="flex-start">{t('Share the document')}</Box>
|
||||
<Text
|
||||
as="h1"
|
||||
id="doc-share-modal-title"
|
||||
$align="flex-start"
|
||||
$size="small"
|
||||
$weight="600"
|
||||
$margin="0"
|
||||
>
|
||||
{t('Share the document')}
|
||||
</Text>
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the share modal')}
|
||||
onClick={onClose}
|
||||
@@ -199,6 +208,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
$textAlign="center"
|
||||
$variation="600"
|
||||
$size="sm"
|
||||
as="p"
|
||||
>
|
||||
{t(
|
||||
'You can view this document but need additional access to see its members or modify settings.',
|
||||
|
||||
+13
-8
@@ -69,10 +69,11 @@ export const ModalConfirmationVersion = ({
|
||||
isOpen
|
||||
closeOnClickOutside
|
||||
onClose={() => onClose()}
|
||||
aria-describedby="modal-confirmation-version-title"
|
||||
rightActions={
|
||||
<>
|
||||
<Button
|
||||
aria-label={t('Close the modal')}
|
||||
aria-label={`${t('Cancel')} - ${t('Warning')}`}
|
||||
color="secondary"
|
||||
fullWidth
|
||||
onClick={() => onClose()}
|
||||
@@ -102,20 +103,24 @@ export const ModalConfirmationVersion = ({
|
||||
}
|
||||
size={ModalSize.SMALL}
|
||||
title={
|
||||
<Text $size="h6" $align="flex-start" $variation="1000">
|
||||
<Text
|
||||
as="h1"
|
||||
$margin="0"
|
||||
id="modal-confirmation-version-title"
|
||||
$size="h6"
|
||||
$align="flex-start"
|
||||
$variation="1000"
|
||||
>
|
||||
{t('Warning')}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
aria-label={t('Modal confirmation to restore the version')}
|
||||
className="--docs--modal-confirmation-version"
|
||||
>
|
||||
<Box className="--docs--modal-confirmation-version">
|
||||
<Box>
|
||||
<Text $variation="600">
|
||||
<Text $variation="600" as="p">
|
||||
{t('Your current document will revert to this version.')}
|
||||
</Text>
|
||||
<Text $variation="600">
|
||||
<Text $variation="600" as="p">
|
||||
{t('If a member is editing, his works can be lost.')}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
+9
@@ -48,6 +48,7 @@ export const ModalSelectVersion = ({
|
||||
closeOnClickOutside={true}
|
||||
size={ModalSize.EXTRA_LARGE}
|
||||
onClose={onClose}
|
||||
aria-describedby="modal-select-version-title"
|
||||
>
|
||||
<NoPaddingStyle />
|
||||
<Box
|
||||
@@ -58,6 +59,14 @@ export const ModalSelectVersion = ({
|
||||
$maxHeight="calc(100vh - 2em - 12px)"
|
||||
$overflow="hidden"
|
||||
>
|
||||
<Text
|
||||
as="h1"
|
||||
$margin="0"
|
||||
id="modal-select-version-title"
|
||||
className="sr-only"
|
||||
>
|
||||
{t('Version history')}
|
||||
</Text>
|
||||
<Box
|
||||
$css={css`
|
||||
display: flex;
|
||||
|
||||
@@ -70,7 +70,6 @@ export const DocsGrid = ({
|
||||
>
|
||||
<DocsGridLoader isLoading={isRefetching || loading} />
|
||||
<Card
|
||||
role="grid"
|
||||
data-testid="docs-grid"
|
||||
$height="100%"
|
||||
$width="100%"
|
||||
@@ -84,7 +83,7 @@ export const DocsGrid = ({
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
as="h4"
|
||||
as="h2"
|
||||
$size="h4"
|
||||
$variation="1000"
|
||||
$margin={{ top: '0px', bottom: '10px' }}
|
||||
@@ -101,48 +100,57 @@ export const DocsGrid = ({
|
||||
)}
|
||||
{hasDocs && (
|
||||
<Box $gap="6px" $overflow="auto">
|
||||
<Box
|
||||
$direction="row"
|
||||
$padding={{ horizontal: 'xs' }}
|
||||
$gap="10px"
|
||||
data-testid="docs-grid-header"
|
||||
>
|
||||
<Box $flex={flexLeft} $padding="3xs">
|
||||
<Text $size="xs" $variation="600" $weight="500">
|
||||
{t('Name')}
|
||||
</Text>
|
||||
</Box>
|
||||
{isDesktop && (
|
||||
<Box $flex={flexRight} $padding={{ vertical: '3xs' }}>
|
||||
<Text $size="xs" $weight="500" $variation="600">
|
||||
{t('Updated at')}
|
||||
</Text>
|
||||
<Box role="grid">
|
||||
<Box role="rowgroup">
|
||||
<Box
|
||||
$direction="row"
|
||||
$padding={{ horizontal: 'xs' }}
|
||||
$gap="10px"
|
||||
data-testid="docs-grid-header"
|
||||
role="row"
|
||||
>
|
||||
<Box $flex={flexLeft} $padding="3xs" role="columnheader">
|
||||
<Text $size="xs" $variation="600" $weight="500">
|
||||
{t('Name')}
|
||||
</Text>
|
||||
</Box>
|
||||
{isDesktop && (
|
||||
<Box
|
||||
$flex={flexRight}
|
||||
$padding={{ vertical: '3xs' }}
|
||||
role="columnheader"
|
||||
>
|
||||
<Text $size="xs" $weight="500" $variation="600">
|
||||
{t('Updated at')}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box role="rowgroup">
|
||||
{isDesktop ? (
|
||||
<DraggableDocGridContentList docs={docs} />
|
||||
) : (
|
||||
<DocGridContentList docs={docs} />
|
||||
)}
|
||||
</Box>
|
||||
{hasNextPage && !loading && (
|
||||
<InView
|
||||
data-testid="infinite-scroll-trigger"
|
||||
as="div"
|
||||
onChange={loadMore}
|
||||
>
|
||||
{!isFetching && hasNextPage && (
|
||||
<Button
|
||||
onClick={() => void fetchNextPage()}
|
||||
color="primary-text"
|
||||
>
|
||||
{t('More docs')}
|
||||
</Button>
|
||||
)}
|
||||
</InView>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{isDesktop ? (
|
||||
<DraggableDocGridContentList docs={docs} />
|
||||
) : (
|
||||
<DocGridContentList docs={docs} />
|
||||
)}
|
||||
|
||||
{hasNextPage && !loading && (
|
||||
<InView
|
||||
data-testid="infinite-scroll-trigger"
|
||||
as="div"
|
||||
onChange={loadMore}
|
||||
>
|
||||
{!isFetching && hasNextPage && (
|
||||
<Button
|
||||
onClick={() => void fetchNextPage()}
|
||||
color="primary-text"
|
||||
>
|
||||
{t('More docs')}
|
||||
</Button>
|
||||
)}
|
||||
</InView>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -53,67 +53,76 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
||||
`}
|
||||
className="--docs--doc-grid-item"
|
||||
>
|
||||
<StyledLink
|
||||
<Box
|
||||
$flex={flexLeft}
|
||||
role="gridcell"
|
||||
$css={css`
|
||||
flex: ${flexLeft};
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
`}
|
||||
href={`/docs/${doc.id}`}
|
||||
>
|
||||
<Box
|
||||
data-testid={`docs-grid-name-${doc.id}`}
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap={spacingsTokens.xs}
|
||||
$padding={{ right: isDesktop ? 'md' : '3xs' }}
|
||||
$maxWidth="100%"
|
||||
<StyledLink
|
||||
$css={css`
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
`}
|
||||
href={`/docs/${doc.id}`}
|
||||
>
|
||||
<SimpleDocItem isPinned={doc.is_favorite} doc={doc} />
|
||||
{isShared && (
|
||||
<Box
|
||||
$padding={{ top: !isDesktop ? '4xs' : undefined }}
|
||||
$css={
|
||||
!isDesktop
|
||||
? css`
|
||||
align-self: flex-start;
|
||||
`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{dragMode && (
|
||||
<Icon
|
||||
$theme="greyscale"
|
||||
$variation="600"
|
||||
$size="14px"
|
||||
iconName={isPublic ? 'public' : 'vpn_lock'}
|
||||
/>
|
||||
)}
|
||||
{!dragMode && (
|
||||
<Tooltip
|
||||
content={
|
||||
<Text $textAlign="center" $variation="000">
|
||||
{isPublic
|
||||
? t('Accessible to anyone')
|
||||
: t('Accessible to authenticated users')}
|
||||
</Text>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
$theme="greyscale"
|
||||
$variation="600"
|
||||
$size="14px"
|
||||
iconName={isPublic ? 'public' : 'vpn_lock'}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</StyledLink>
|
||||
<Box
|
||||
data-testid={`docs-grid-name-${doc.id}`}
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap={spacingsTokens.xs}
|
||||
$padding={{ right: isDesktop ? 'md' : '3xs' }}
|
||||
$maxWidth="100%"
|
||||
>
|
||||
<SimpleDocItem isPinned={doc.is_favorite} doc={doc} />
|
||||
{isShared && (
|
||||
<Box
|
||||
$padding={{ top: !isDesktop ? '4xs' : undefined }}
|
||||
$css={
|
||||
!isDesktop
|
||||
? css`
|
||||
align-self: flex-start;
|
||||
`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{dragMode && (
|
||||
<Icon
|
||||
$theme="greyscale"
|
||||
$variation="600"
|
||||
$size="14px"
|
||||
iconName={isPublic ? 'public' : 'vpn_lock'}
|
||||
/>
|
||||
)}
|
||||
{!dragMode && (
|
||||
<Tooltip
|
||||
content={
|
||||
<Text $textAlign="center" $variation="000">
|
||||
{isPublic
|
||||
? t('Accessible to anyone')
|
||||
: t('Accessible to authenticated users')}
|
||||
</Text>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
$theme="greyscale"
|
||||
$variation="600"
|
||||
$size="14px"
|
||||
iconName={isPublic ? 'public' : 'vpn_lock'}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</StyledLink>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
$flex={flexRight}
|
||||
@@ -121,6 +130,7 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
||||
$align="center"
|
||||
$justify={isDesktop ? 'space-between' : 'flex-end'}
|
||||
$gap="32px"
|
||||
role="gridcell"
|
||||
>
|
||||
{isDesktop && (
|
||||
<StyledLink href={`/docs/${doc.id}`}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
import { createGlobalStyle, css } from 'styled-components';
|
||||
|
||||
import { Box } from '@/components';
|
||||
|
||||
@@ -32,6 +32,9 @@ export const DocsGridLoader = ({ isLoading }: DocsGridLoaderProps) => {
|
||||
$zIndex={998}
|
||||
$position="absolute"
|
||||
className="--docs--doc-grid-loader"
|
||||
$css={css`
|
||||
pointer-events: none;
|
||||
`}
|
||||
>
|
||||
<Loader />
|
||||
</Box>
|
||||
|
||||
@@ -19,6 +19,7 @@ export const Draggable = <T,>(props: DraggableProps<T>) => {
|
||||
{...attributes}
|
||||
data-testid={`draggable-doc-${props.id}`}
|
||||
className="--docs--grid-draggable"
|
||||
role="presentation"
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,7 @@ export const Droppable = ({
|
||||
<Box
|
||||
ref={setNodeRef}
|
||||
data-testid={`droppable-doc-${id}`}
|
||||
role="presentation"
|
||||
$css={css`
|
||||
border-radius: 4px;
|
||||
background-color: ${enableHover
|
||||
|
||||
@@ -43,6 +43,13 @@ export const Header = () => {
|
||||
href="/"
|
||||
data-testid="header-logo-link"
|
||||
aria-label={t('Back to homepage')}
|
||||
$css={css`
|
||||
outline: none;
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--c--theme--colors--primary-400) !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
$align="center"
|
||||
|
||||
@@ -41,15 +41,7 @@ export const LanguagePicker = () => {
|
||||
showArrow
|
||||
label={t('Select language')}
|
||||
buttonCss={css`
|
||||
&:hover {
|
||||
background-color: var(
|
||||
--c--components--button--primary-text--background--color-hover
|
||||
);
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--c--theme--colors--primary-500);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
transition: all 0.15s ease-in-out !important;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 0.6rem;
|
||||
& > div {
|
||||
|
||||
+3
-2
@@ -85,8 +85,9 @@ export const LeftPanelTargetFilters = () => {
|
||||
background-color: ${colorsTokens['greyscale-100']};
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid ${colorsTokens['primary-500']};
|
||||
outline-offset: 2px;
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 2px ${colorsTokens['primary-500']} !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
|
||||
+8
-9
@@ -31,27 +31,26 @@ export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => {
|
||||
.pinned-actions {
|
||||
opacity: ${isDesktop ? 0 : 1};
|
||||
}
|
||||
&:hover,
|
||||
&:hover {
|
||||
background-color: ${colorsTokens['greyscale-100']};
|
||||
}
|
||||
&:focus-within {
|
||||
cursor: pointer;
|
||||
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
box-shadow: 0 0 0 2px ${colorsTokens['primary-500']} !important;
|
||||
.pinned-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid ${colorsTokens['primary-500']};
|
||||
outline-offset: 2px;
|
||||
border-radius: ${spacingsTokens['3xs']};
|
||||
}
|
||||
`}
|
||||
key={doc.id}
|
||||
className="--docs--left-panel-favorite-item"
|
||||
>
|
||||
<StyledLink
|
||||
href={`/docs/${doc.id}`}
|
||||
$css="overflow: auto;"
|
||||
$css={css`
|
||||
overflow: auto;
|
||||
outline: none !important;
|
||||
`}
|
||||
aria-label={`${doc.title}, ${t('Updated')} ${DateTime.fromISO(doc.updated_at).toRelative()}`}
|
||||
>
|
||||
<SimpleDocItem showAccesses doc={doc} />
|
||||
|
||||
+15
-11
@@ -44,17 +44,21 @@ export const LeftPanelFavorites = () => {
|
||||
>
|
||||
{t('Pinned documents')}
|
||||
</Text>
|
||||
<InfiniteScroll
|
||||
as="ul"
|
||||
hasMore={docs.hasNextPage}
|
||||
isLoading={docs.isFetchingNextPage}
|
||||
next={() => void docs.fetchNextPage()}
|
||||
$padding="none"
|
||||
>
|
||||
{favoriteDocs.map((doc) => (
|
||||
<LeftPanelFavoriteItem key={doc.id} doc={doc} />
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
<Box>
|
||||
<Box as="ul" $padding="none">
|
||||
{favoriteDocs.map((doc) => (
|
||||
<LeftPanelFavoriteItem key={doc.id} doc={doc} />
|
||||
))}
|
||||
</Box>
|
||||
{docs.hasNextPage && (
|
||||
<InfiniteScroll
|
||||
hasMore={docs.hasNextPage}
|
||||
isLoading={docs.isFetchingNextPage}
|
||||
next={() => void docs.fetchNextPage()}
|
||||
$padding="none"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"Close the search modal": "Such-Modal schließen",
|
||||
"Close the share modal": "Freigabe-Modal schließen",
|
||||
"Close the version history modal": "Versionsverlauf-Modal schließen",
|
||||
"Close the download modal": "Download-Modal schließen",
|
||||
"Collaborate": "Zusammenarbeiten",
|
||||
"Collaborate and write in real time, without layout constraints.": "In Echtzeit und ohne Layout-Beschränkungen schreiben und zusammenarbeiten.",
|
||||
"Collaborative writing, Simplified.": "Kollaboratives Schreiben, vereinfacht.",
|
||||
@@ -302,8 +303,11 @@
|
||||
"Document accessible to any connected person": "Dokument für jeden angemeldeten Benutzer zugänglich",
|
||||
"Document duplicated successfully!": "Dokument erfolgreich dupliziert!",
|
||||
"Document owner": "Besitzer des Dokuments",
|
||||
"Document title": "Dokumenttitel",
|
||||
"Document sections": "Dokumentabschnitte",
|
||||
"Docx": "Docx",
|
||||
"Download": "Herunterladen",
|
||||
"Download the document": "Dokument herunterladen",
|
||||
"Download anyway": "Trotzdem herunterladen",
|
||||
"Download your document in a .docx or .pdf format.": "Ihr Dokument als .docx- oder .pdf-Datei herunterladen.",
|
||||
"Duplicate": "Duplizieren",
|
||||
@@ -451,6 +455,7 @@
|
||||
"en": {
|
||||
"translation": {
|
||||
"Back to homepage": "Back to Docs homepage",
|
||||
"Document title": "Document title",
|
||||
"Search docs": "Search docs",
|
||||
"More options": "More options",
|
||||
"Pinned documents": "Pinned documents",
|
||||
@@ -459,6 +464,9 @@
|
||||
"Close the search modal": "Close the search modal",
|
||||
"Close the share modal": "Close the share modal",
|
||||
"Close the version history modal": "Close the version history modal",
|
||||
"Close the download modal": "Close the download modal",
|
||||
"Cancel the deletion": "Cancel the deletion",
|
||||
"Cancel the download": "Cancel the download",
|
||||
"Open actions menu for document: {{title}}": "Open actions menu for document: {{title}}",
|
||||
"Open the menu of actions for the document: {{title}}": "Open the menu of actions for the document: {{title}}",
|
||||
"Share with {{count}} users_one": "Share with {{count}} user",
|
||||
@@ -501,10 +509,13 @@
|
||||
"Close the search modal": "Cerrar modal de búsqueda",
|
||||
"Close the share modal": "Cerrar modal de compartir",
|
||||
"Close the version history modal": "Cerrar modal de historial de versiones",
|
||||
"Close the download modal": "Cerrar modal de descarga",
|
||||
"Collaborate": "Colabora",
|
||||
"Collaborate and write in real time, without layout constraints.": "Colaborar y escribir en tiempo real, sin restricciones de diseño.",
|
||||
"Collaborative writing, Simplified.": "Escritura colaborativa, más sencilla.",
|
||||
"Confirm deletion": "Confirmar borrado",
|
||||
"Cancel the deletion": "Cancelar la eliminación",
|
||||
"Cancel the download": "Cancelar la descarga",
|
||||
"Connected": "Conectado",
|
||||
"Content modal to delete document": "Modal para eliminar el documento",
|
||||
"Content modal to export the document": "Ventana emergente para exportar el documento",
|
||||
@@ -527,8 +538,11 @@
|
||||
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs: su nuevo compañero para colaborar en documentos de forma eficiente, intuitiva y segura.",
|
||||
"Document accessible to any connected person": "Documento accesible a cualquier persona conectada",
|
||||
"Document owner": "Propietario del documento",
|
||||
"Document title": "Título del documento",
|
||||
"Document sections": "Secciones del documento",
|
||||
"Docx": "Docx",
|
||||
"Download": "Descargar",
|
||||
"Download the document": "Descargar el documento",
|
||||
"Download anyway": "Descargar de todos modos",
|
||||
"Download your document in a .docx or .pdf format.": "Descargue su documento en formato .docx o .pdf.",
|
||||
"Editor": "Editor",
|
||||
@@ -697,6 +711,8 @@
|
||||
"Collaborative writing, Simplified.": "L'écriture collaborative simplifiée.",
|
||||
"Confirm": "Confirmez",
|
||||
"Confirm deletion": "Confirmer la suppression",
|
||||
"Cancel the deletion": "Annuler la suppression",
|
||||
"Cancel the download": "Annuler le téléchargement",
|
||||
"Confirmation button": "Bouton de confirmation",
|
||||
"Connected": "Connecté",
|
||||
"Content modal to delete document": "Contenu modal pour supprimer le document",
|
||||
@@ -726,10 +742,12 @@
|
||||
"Document duplicated successfully!": "Document dupliqué avec succès !",
|
||||
"Document emoji icon": "Émoticônes du document",
|
||||
"Document owner": "Propriétaire du document",
|
||||
"Document title": "Titre du document",
|
||||
"Document sections": "Sections du document",
|
||||
"Document visibility": "Visibilité du document",
|
||||
"Docx": "Docx",
|
||||
"Download": "Télécharger",
|
||||
"Download the document": "Télécharger le document",
|
||||
"Download anyway": "Télécharger malgré tout",
|
||||
"Download your document in a .docx or .pdf format.": "Téléchargez votre document au format .docx ou .pdf.",
|
||||
"Duplicate": "Dupliquer",
|
||||
@@ -1079,10 +1097,13 @@
|
||||
"Close the search modal": "Sluit het zoek venster",
|
||||
"Close the share modal": "Sluit het delen venster",
|
||||
"Close the version history modal": "Sluit het versiehistorie venster",
|
||||
"Close the download modal": "Sluit het download venster",
|
||||
"Collaborate": "Samenwerken",
|
||||
"Collaborate and write in real time, without layout constraints.": "Samenwerken en schrijven in realtime, zonder lay-out beperkingen.",
|
||||
"Collaborative writing, Simplified.": "Vereenvoudigd samenwerkend",
|
||||
"Confirm deletion": "Bevestig verwijdering",
|
||||
"Cancel the deletion": "Annuleer de verwijdering",
|
||||
"Cancel the download": "Annuleer de download",
|
||||
"Connected": "Verbonden",
|
||||
"Content modal to delete document": "Content venster om het document te verwijderen",
|
||||
"Content modal to export the document": "Content venster om document te exporteren",
|
||||
@@ -1105,8 +1126,11 @@
|
||||
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs: Je nieuwe metgezel om efficiënt, intuïtief en veilig samen te werken aan documenten.",
|
||||
"Document accessible to any connected person": "Document is toegankelijk voor ieder verbonden persoon",
|
||||
"Document owner": "Document eigenaar",
|
||||
"Document title": "Documenttitel",
|
||||
"Document sections": "Document secties",
|
||||
"Docx": "Docx",
|
||||
"Download": "Download",
|
||||
"Download the document": "Document downloaden",
|
||||
"Download anyway": "Download alsnog",
|
||||
"Download your document in a .docx or .pdf format.": "Download jouw document in .docx of .pdf formaat.",
|
||||
"Editor": "Bewerker",
|
||||
|
||||
@@ -82,3 +82,16 @@ main ::-webkit-scrollbar-thumb:hover,
|
||||
nextjs-portal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Screen reader only - visually hidden but accessible to screen readers */
|
||||
.sr-only {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
padding: 0 !important;
|
||||
margin: -1px !important;
|
||||
overflow: hidden !important;
|
||||
clip-path: inset(50%) !important;
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
@@ -16,16 +16,16 @@ backend:
|
||||
replicas: 1
|
||||
envVars:
|
||||
COLLABORATION_SERVER_SECRET: my-secret
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS: https://impress.127.0.0.1.nip.io
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS: https://docs.127.0.0.1.nip.io
|
||||
DJANGO_CONFIGURATION: Feature
|
||||
DJANGO_ALLOWED_HOSTS: impress.127.0.0.1.nip.io
|
||||
DJANGO_ALLOWED_HOSTS: docs.127.0.0.1.nip.io
|
||||
DJANGO_SERVER_TO_SERVER_API_TOKENS: secret-api-key
|
||||
DJANGO_SECRET_KEY: *djangoSecretKey
|
||||
DJANGO_SETTINGS_MODULE: impress.settings
|
||||
DJANGO_SUPERUSER_PASSWORD: admin
|
||||
DJANGO_EMAIL_BRAND_NAME: "La Suite Numérique"
|
||||
DJANGO_EMAIL_HOST: "mailcatcher"
|
||||
DJANGO_EMAIL_LOGO_IMG: https://impress.127.0.0.1.nip.io/assets/logo-suite-numerique.png
|
||||
DJANGO_EMAIL_LOGO_IMG: https://docs.127.0.0.1.nip.io/assets/logo-suite-numerique.png
|
||||
DJANGO_EMAIL_PORT: 1025
|
||||
DJANGO_EMAIL_USE_SSL: False
|
||||
LOGGING_LEVEL_HANDLERS_CONSOLE: ERROR
|
||||
@@ -33,29 +33,38 @@ backend:
|
||||
LOGGING_LEVEL_LOGGERS_APP: INFO
|
||||
OIDC_USERINFO_SHORTNAME_FIELD: "given_name"
|
||||
OIDC_USERINFO_FULLNAME_FIELDS: "given_name,usual_name"
|
||||
OIDC_OP_JWKS_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/certs
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/auth
|
||||
OIDC_OP_TOKEN_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/token
|
||||
OIDC_OP_USER_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/userinfo
|
||||
OIDC_OP_LOGOUT_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/logout
|
||||
OIDC_RP_CLIENT_ID: impress
|
||||
OIDC_OP_JWKS_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/certs
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/auth
|
||||
OIDC_OP_TOKEN_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/token
|
||||
OIDC_OP_USER_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/userinfo
|
||||
OIDC_OP_LOGOUT_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/logout
|
||||
OIDC_RP_CLIENT_ID: docs
|
||||
OIDC_RP_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
|
||||
OIDC_RP_SIGN_ALGO: RS256
|
||||
OIDC_RP_SCOPES: "openid email"
|
||||
LOGIN_REDIRECT_URL: https://impress.127.0.0.1.nip.io
|
||||
LOGIN_REDIRECT_URL_FAILURE: https://impress.127.0.0.1.nip.io
|
||||
LOGOUT_REDIRECT_URL: https://impress.127.0.0.1.nip.io
|
||||
DB_HOST: postgres-postgresql
|
||||
DB_NAME: impress
|
||||
DB_USER: dinum
|
||||
DB_PASSWORD: pass
|
||||
LOGIN_REDIRECT_URL: https://docs.127.0.0.1.nip.io
|
||||
LOGIN_REDIRECT_URL_FAILURE: https://docs.127.0.0.1.nip.io
|
||||
LOGOUT_REDIRECT_URL: https://docs.127.0.0.1.nip.io
|
||||
DB_HOST: dev-backend-postgres
|
||||
DB_NAME:
|
||||
secretKeyRef:
|
||||
name: dev-backend-postgres
|
||||
key: database
|
||||
DB_USER:
|
||||
secretKeyRef:
|
||||
name: dev-backend-postgres
|
||||
key: username
|
||||
DB_PASSWORD:
|
||||
secretKeyRef:
|
||||
name: dev-backend-postgres
|
||||
key: password
|
||||
DB_PORT: 5432
|
||||
REDIS_URL: redis://default:pass@redis-master:6379/1
|
||||
DJANGO_CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
|
||||
AWS_S3_ENDPOINT_URL: http://minio.impress.svc.cluster.local:9000
|
||||
AWS_S3_ACCESS_KEY_ID: root
|
||||
REDIS_URL: redis://user:pass@dev-backend-redis:6379/1
|
||||
DJANGO_CELERY_BROKER_URL: redis://user:pass@dev-backend-redis:6379/1
|
||||
AWS_S3_ENDPOINT_URL: http://dev-backend-minio.impress.svc.cluster.local:9000
|
||||
AWS_S3_ACCESS_KEY_ID: dinum
|
||||
AWS_S3_SECRET_ACCESS_KEY: password
|
||||
AWS_STORAGE_BUCKET_NAME: impress-media-storage
|
||||
AWS_STORAGE_BUCKET_NAME: docs-media-storage
|
||||
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
||||
Y_PROVIDER_API_BASE_URL: http://impress-y-provider:443/api/
|
||||
Y_PROVIDER_API_KEY: my-secret
|
||||
@@ -73,8 +82,7 @@ backend:
|
||||
|
||||
echo "Database is ready"
|
||||
|
||||
python manage.py migrate --no-input &&
|
||||
python manage.py create_demo --force
|
||||
python manage.py migrate --no-input
|
||||
restartPolicy: Never
|
||||
|
||||
command:
|
||||
@@ -120,7 +128,7 @@ backend:
|
||||
frontend:
|
||||
envVars:
|
||||
PORT: 8080
|
||||
NEXT_PUBLIC_API_ORIGIN: https://impress.127.0.0.1.nip.io
|
||||
NEXT_PUBLIC_API_ORIGIN: https://docs.127.0.0.1.nip.io
|
||||
|
||||
replicas: 1
|
||||
command:
|
||||
@@ -141,27 +149,29 @@ yProvider:
|
||||
tag: "latest"
|
||||
|
||||
envVars:
|
||||
COLLABORATION_BACKEND_BASE_URL: https://impress.127.0.0.1.nip.io
|
||||
COLLABORATION_BACKEND_BASE_URL: https://docs.127.0.0.1.nip.io
|
||||
COLLABORATION_LOGGING: true
|
||||
COLLABORATION_SERVER_ORIGIN: https://impress.127.0.0.1.nip.io
|
||||
COLLABORATION_SERVER_ORIGIN: https://docs.127.0.0.1.nip.io
|
||||
COLLABORATION_SERVER_SECRET: my-secret
|
||||
Y_PROVIDER_API_KEY: my-secret
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
host: impress.127.0.0.1.nip.io
|
||||
host: docs.127.0.0.1.nip.io
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: 10m
|
||||
|
||||
ingressCollaborationWS:
|
||||
enabled: true
|
||||
host: impress.127.0.0.1.nip.io
|
||||
host: docs.127.0.0.1.nip.io
|
||||
|
||||
ingressCollaborationApi:
|
||||
enabled: true
|
||||
host: impress.127.0.0.1.nip.io
|
||||
host: docs.127.0.0.1.nip.io
|
||||
|
||||
ingressAdmin:
|
||||
enabled: true
|
||||
host: impress.127.0.0.1.nip.io
|
||||
host: docs.127.0.0.1.nip.io
|
||||
|
||||
posthog:
|
||||
ingress:
|
||||
@@ -172,14 +182,14 @@ posthog:
|
||||
|
||||
ingressMedia:
|
||||
enabled: true
|
||||
host: impress.127.0.0.1.nip.io
|
||||
host: docs.127.0.0.1.nip.io
|
||||
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/auth-url: https://impress.127.0.0.1.nip.io/api/v1.0/documents/media-auth/
|
||||
nginx.ingress.kubernetes.io/auth-url: https://docs.127.0.0.1.nip.io/api/v1.0/documents/media-auth/
|
||||
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
|
||||
nginx.ingress.kubernetes.io/upstream-vhost: minio.impress.svc.cluster.local:9000
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /impress-media-storage/$1
|
||||
nginx.ingress.kubernetes.io/upstream-vhost: dev-backend-minio.impress.svc.cluster.local:9000
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /docs-media-storage/$1
|
||||
|
||||
serviceMedia:
|
||||
host: minio.impress.svc.cluster.local
|
||||
host: dev-backend-minio.impress.svc.cluster.local
|
||||
port: 9000
|
||||
|
||||
+59
-75
@@ -4,91 +4,75 @@ environments:
|
||||
- version: 3.6.0
|
||||
---
|
||||
repositories:
|
||||
- name: bitnami
|
||||
url: registry-1.docker.io/bitnamicharts
|
||||
oci: true
|
||||
- name: dev-backends
|
||||
url: https://suitenumerique.github.io/helm-dev-backend
|
||||
---
|
||||
|
||||
releases:
|
||||
- name: keycloak
|
||||
installed: {{ eq .Environment.Name "dev" | toYaml }}
|
||||
missingFileHandler: Warn
|
||||
- name: dev-backend
|
||||
namespace: {{ .Namespace }}
|
||||
chart: bitnami/keycloak
|
||||
version: 17.3.6
|
||||
chart: dev-backends/dev-backend
|
||||
version: 0.0.2
|
||||
values:
|
||||
- postgresql:
|
||||
auth:
|
||||
username: keycloak
|
||||
password: keycloak
|
||||
database: keycloak
|
||||
- extraEnvVars:
|
||||
- name: KEYCLOAK_EXTRA_ARGS
|
||||
value: "--import-realm"
|
||||
- name: KC_HOSTNAME_URL
|
||||
value: https://docs-keycloak.127.0.0.1.nip.io
|
||||
- extraVolumes:
|
||||
- name: import
|
||||
configMap:
|
||||
name: docs-keycloak
|
||||
- extraVolumeMounts:
|
||||
- name: import
|
||||
mountPath: /opt/bitnami/keycloak/data/import/
|
||||
- auth:
|
||||
adminUser: su
|
||||
adminPassword: su
|
||||
- proxy: edge
|
||||
- ingress:
|
||||
- postgres:
|
||||
enabled: true
|
||||
hostname: docs-keycloak.127.0.0.1.nip.io
|
||||
- extraDeploy:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: docs-keycloak
|
||||
namespace: {{ .Namespace }}
|
||||
data:
|
||||
impress.json: |
|
||||
{{ readFile "../../docker/auth/realm.json" | replace "http://localhost:3200" "https://impress.127.0.0.1.nip.io" | indent 14 }}
|
||||
|
||||
- name: postgres
|
||||
installed: {{ eq .Environment.Name "dev" | toYaml }}
|
||||
namespace: {{ .Namespace }}
|
||||
chart: bitnami/postgresql
|
||||
version: 13.1.5
|
||||
values:
|
||||
- auth:
|
||||
name: postgres
|
||||
#serviceNameOverride: postgres
|
||||
image: postgres:16-alpine
|
||||
username: dinum
|
||||
password: pass
|
||||
database: impress
|
||||
- tls:
|
||||
database: docs
|
||||
size: 1Gi
|
||||
- redis:
|
||||
enabled: true
|
||||
autoGenerated: true
|
||||
|
||||
- name: minio
|
||||
installed: {{ eq .Environment.Name "dev" | toYaml }}
|
||||
namespace: {{ .Namespace }}
|
||||
chart: bitnami/minio
|
||||
version: 12.10.10
|
||||
values:
|
||||
- auth:
|
||||
rootUser: root
|
||||
rootPassword: password
|
||||
- provisioning:
|
||||
enabled: true
|
||||
buckets:
|
||||
- name: impress-media-storage
|
||||
versioning: true
|
||||
|
||||
- name: redis
|
||||
installed: {{ eq .Environment.Name "dev" | toYaml }}
|
||||
namespace: {{ .Namespace }}
|
||||
chart: bitnami/redis
|
||||
version: 20.6.2
|
||||
values:
|
||||
- auth:
|
||||
name: redis
|
||||
image: redis:8.2-alpine
|
||||
username: user
|
||||
password: pass
|
||||
architecture: standalone
|
||||
- minio:
|
||||
enabled: true
|
||||
image: minio/minio
|
||||
name: minio
|
||||
ingress:
|
||||
enabled: true
|
||||
hostname: docs-minio.127.0.0.1.nip.io
|
||||
tls:
|
||||
enabled: true
|
||||
secretName: docs-tls
|
||||
consoleIngress:
|
||||
enabled: true
|
||||
hostname: docs-minio-console.127.0.0.1.nip.io
|
||||
tls:
|
||||
enabled: true
|
||||
secretName: docs-tls
|
||||
username: dinum
|
||||
password: password
|
||||
bucket: docs-media-storage
|
||||
versioning: true
|
||||
size: 1Gi
|
||||
- keycloak:
|
||||
enabled: true
|
||||
image: quay.io/keycloak/keycloak:20.0.1
|
||||
name: keycloak
|
||||
#serviceNameOverride: keycloak
|
||||
hostname: docs-keycloak.127.0.0.1.nip.io
|
||||
username: admin
|
||||
password: pass
|
||||
tls:
|
||||
enabled: true
|
||||
secretName: docs-tls
|
||||
db:
|
||||
username: dinum
|
||||
password: pass
|
||||
database: keycloak
|
||||
size: 1Gi
|
||||
image: postgres:16-alpine
|
||||
realm:
|
||||
name: docs
|
||||
username: docs
|
||||
password: docs
|
||||
email: docs@example.com
|
||||
|
||||
|
||||
- name: impress
|
||||
version: {{ .Values.version }}
|
||||
|
||||
Reference in New Issue
Block a user