Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-intelligent-assistant': patch
---

Improve notebook UI: enhance AddDocumentModal, OverwriteConfirmModal, NotebookCard, and NotebookView components with better UX text, document count formatting, and fix translation type errors
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ test.describe('Intelligent assistant notebooks', () => {
await uploadModal.selectFilesViaBrowsePicker(
notebookElevenFileStagingPaths(),
);
await expect(uploadModal.dialog().getByRole('alert')).toContainText(
await expect(uploadModal.errorAlert()).toContainText(
substituteNotebookTemplate(
translations['notebook.upload.error.tooManyFiles'],
{ max: NOTEBOOK_SESSION_MAX_DOCUMENTS },
Expand All @@ -115,7 +115,7 @@ test.describe('Intelligent assistant notebooks', () => {
await uploadModal.selectFilesViaBrowsePicker([
notebookUnsupportedTypeFixturePath(),
]);
await expect(uploadModal.dialog().getByRole('alert')).toContainText(
await expect(uploadModal.errorAlert()).toContainText(
translations['notebook.upload.error.unsupportedType'],
);
await uploadModal.clickCancel();
Expand All @@ -136,19 +136,27 @@ test.describe('Intelligent assistant notebooks', () => {
await notebooks.clickOpenUploadDocumentModal();
uploadModal = notebooks.uploadDocumentModal();
await uploadModal.selectFilesViaBrowsePicker([absolutePath]);
await uploadModal.clickAddFilesForStagedCount(1);

const overwriteModal = notebooks.notebookOverwriteConfirmModal();
await overwriteModal.expectDialogVisible();
await overwriteModal.expectListedOverwriteFile(fileName);
await overwriteModal.clickCancel();
await sharedPage.waitForTimeout(200);
await overwriteModal.clickBack();
await uploadModal.clickCancel();

await notebooks.deleteFirstListedDocumentFromSidebarOverflowMenu();
await notebooks.expectNotebookEditorUploadResourceButtonVisible();
});

test('grid: close editor, rename, delete', async () => {
test('grid: close editor, rename, delete', async ({}, testInfo) => {
const { absolutePath } = localeNotebookUpload1Path(testInfo.project.name);

await notebooks.clickOpenUploadDocumentModal();
const uploadModal = notebooks.uploadDocumentModal();
await uploadModal.selectFilesViaBrowsePicker([absolutePath]);
await uploadModal.clickAddFilesForStagedCount(1);
await sharedPage.waitForTimeout(1000);

const untitledBefore = await notebooks.untitledNotebookCards().count();

await notebooks.clickCloseNotebookEditor();
Expand All @@ -157,7 +165,7 @@ test.describe('Intelligent assistant notebooks', () => {
await expect(notebooks.newestUntitledNotebookCard()).toBeVisible();

await notebooks.expectNotebookListShowsDocumentCountSummaryAndUpdatedToday(
0,
1,
);

await notebooks
Expand Down Expand Up @@ -188,8 +196,19 @@ test.describe('Intelligent assistant notebooks', () => {
await notebooks.expectUntitledNotebookCardCount(untitledBefore);
});

test('grid: click card title triggers inline rename', async () => {
test('grid: click card title triggers inline rename', async ({}, testInfo) => {
const { absolutePath, fileName } = localeNotebookUpload1Path(
testInfo.project.name,
);

await notebooks.clickPrimaryNotebookCreate();

await notebooks.clickOpenUploadDocumentModal();
const uploadModal = notebooks.uploadDocumentModal();
await uploadModal.selectFilesViaBrowsePicker([absolutePath]);
await uploadModal.clickAddFilesForStagedCount(1);
await notebooks.expectDocumentFileListedInSidebar(fileName);

await notebooks.clickCloseNotebookEditor();

const card = notebooks.newestUntitledNotebookCard();
Expand All @@ -215,8 +234,19 @@ test.describe('Intelligent assistant notebooks', () => {
await notebooks.expectNotebookCardAbsent(newName);
});

test('grid: Escape cancels inline rename', async () => {
test('grid: Escape cancels inline rename', async ({}, testInfo) => {
const { absolutePath, fileName } = localeNotebookUpload1Path(
testInfo.project.name,
);

await notebooks.clickPrimaryNotebookCreate();

await notebooks.clickOpenUploadDocumentModal();
const uploadModal = notebooks.uploadDocumentModal();
await uploadModal.selectFilesViaBrowsePicker([absolutePath]);
await uploadModal.clickAddFilesForStagedCount(1);
await notebooks.expectDocumentFileListedInSidebar(fileName);

await notebooks.clickCloseNotebookEditor();

const card = notebooks.newestUntitledNotebookCard();
Expand Down Expand Up @@ -245,8 +275,19 @@ test.describe('Intelligent assistant notebooks', () => {
await confirmDelete.confirmDeletion();
});

test('grid: blur saves inline rename', async () => {
test('grid: blur saves inline rename', async ({}, testInfo) => {
const { absolutePath, fileName } = localeNotebookUpload1Path(
testInfo.project.name,
);

await notebooks.clickPrimaryNotebookCreate();

await notebooks.clickOpenUploadDocumentModal();
const uploadModal = notebooks.uploadDocumentModal();
await uploadModal.selectFilesViaBrowsePicker([absolutePath]);
await uploadModal.clickAddFilesForStagedCount(1);
await notebooks.expectDocumentFileListedInSidebar(fileName);

await notebooks.clickCloseNotebookEditor();

const card = notebooks.newestUntitledNotebookCard();
Expand All @@ -273,8 +314,19 @@ test.describe('Intelligent assistant notebooks', () => {
await notebooks.expectNotebookCardAbsent(newName);
});

test('grid: empty or unchanged name cancels rename', async () => {
test('grid: empty or unchanged name cancels rename', async ({}, testInfo) => {
const { absolutePath, fileName } = localeNotebookUpload1Path(
testInfo.project.name,
);

await notebooks.clickPrimaryNotebookCreate();

await notebooks.clickOpenUploadDocumentModal();
const uploadModal = notebooks.uploadDocumentModal();
await uploadModal.selectFilesViaBrowsePicker([absolutePath]);
await uploadModal.clickAddFilesForStagedCount(1);
await notebooks.expectDocumentFileListedInSidebar(fileName);

await notebooks.clickCloseNotebookEditor();

const card = notebooks.newestUntitledNotebookCard();
Expand Down Expand Up @@ -339,4 +391,47 @@ test.describe('Intelligent assistant notebooks', () => {
await confirmDelete.confirmDeletion();
await notebooks.expectNotebookCardAbsent(newName);
});

test('auto-delete: empty untitled notebook is discarded on close', async () => {
await notebooks.gotoFullscreenNotebooksTab();
const cardsBefore = await notebooks.untitledNotebookCards().count();

await notebooks.clickCreateNotebookFromEmptyList();
await expect(sharedPage).toHaveURL(NOTEBOOK_EDITOR_URL_RE);

await notebooks.clickCloseNotebookEditor();

await notebooks.expectUntitledNotebookCardCount(cardsBefore);
});

test('auto-delete: notebook with uploaded file persists on close', async ({}, testInfo) => {
const { absolutePath, fileName } = localeNotebookUpload1Path(
testInfo.project.name,
);
const cardsBefore = await notebooks.untitledNotebookCards().count();

await notebooks.clickCreateNotebookFromEmptyList();
await expect(sharedPage).toHaveURL(NOTEBOOK_EDITOR_URL_RE);

await notebooks.clickOpenUploadDocumentModal();
const uploadModal = notebooks.uploadDocumentModal();
await uploadModal.selectFilesViaBrowsePicker([absolutePath]);
await uploadModal.clickAddFilesForStagedCount(1);
await notebooks.expectDocumentFileListedInSidebar(fileName);
await sharedPage.waitForTimeout(2000);

await notebooks.clickCloseNotebookEditor();

await notebooks.expectUntitledNotebookCardCount(cardsBefore + 1);

await notebooks
.notebookCardOverflowMenuButton(notebooks.newestUntitledNotebookCard())
.click();
await notebooks.deleteNotebookOverflowMenuItem().click();
const confirmDelete = notebooks.notebookDeleteConfirmationDialog(
NOTEBOOK_UNTITLED_GRID_NAME,
);
await confirmDelete.confirmDeletion();
await notebooks.expectUntitledNotebookCardCount(cardsBefore);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -44,32 +44,24 @@ export class NotebookAddDocumentModalPage {
);
}

separatorBetweenDragZoneAndBrowse(): Locator {
return this.dialog().getByText(this.t['notebook.upload.modal.separator'], {
exact: true,
});
}

browseFilesButton(): Locator {
return this.dialog().getByRole('button', {
name: this.t['notebook.upload.modal.browseButton'],
exact: true,
});
supportedFormatsLabel(): Locator {
return this.dialog().getByText(
this.t['notebook.upload.modal.supportedFormats'],
);
}

acceptedFileTypesParagraph(): Locator {
return this.dialog().getByText(this.t['notebook.upload.modal.infoText'], {
exact: true,
});
maxFileSizeText(): Locator {
return this.dialog().getByText(this.t['notebook.upload.modal.maxFileSize']);
}

addFilesButton(stagedCount: number): Locator {
const label = substituteNotebookTemplate(
this.t['notebook.upload.modal.addButton'],
{
count: stagedCount,
},
);
const label =
stagedCount > 0
? substituteNotebookTemplate(
this.t['notebook.upload.modal.addButton'],
{ count: stagedCount },
)
: this.t['notebook.upload.modal.addButtonEmpty'];
return this.dialog().getByRole('button', { name: label });
}

Expand All @@ -82,9 +74,8 @@ export class NotebookAddDocumentModalPage {
/** Drop-zone copy, “or”, browse button, accepted file types paragraph. */
async expectUploadAreaFullyDescribed(): Promise<void> {
await expect(this.dragAndDropInstructions()).toBeVisible();
await expect(this.separatorBetweenDragZoneAndBrowse()).toBeVisible();
await expect(this.browseFilesButton()).toBeVisible();
await expect(this.acceptedFileTypesParagraph()).toBeVisible();
await expect(this.supportedFormatsLabel()).toBeVisible();
await expect(this.maxFileSizeText()).toBeVisible();
}

async expectModalTitleBarMatchesAriaSnapshot(): Promise<void> {
Expand All @@ -102,7 +93,7 @@ export class NotebookAddDocumentModalPage {
async selectFilesViaBrowsePicker(filePaths: string[]): Promise<void> {
const [fileChooser] = await Promise.all([
this.page.waitForEvent('filechooser'),
this.browseFilesButton().click(),
this.dragAndDropInstructions().click(),
]);
await fileChooser.setFiles(filePaths);
}
Expand Down Expand Up @@ -132,7 +123,7 @@ export class NotebookAddDocumentModalPage {
}

errorAlert(): Locator {
return this.dialog().getByRole('alert');
return this.dialog().locator('[data-ouia-component-type="PF6/Alert"]');
}

async expectValidationAlertsInclude(text: string): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
import { expect, type Locator, type Page } from '@playwright/test';

import type { LightspeedMessages } from '../utils/translations';
import { substituteNotebookTemplate } from '../utils/notebookTranslation';

/**
* “Overwrite Files?” confirmation when staging a file whose name already exists in the notebook (`OverwriteConfirmModal.tsx`).
* "File already exists" confirmation when uploading a file whose name already exists in the notebook (`OverwriteConfirmModal.tsx`).
*/
export class NotebookOverwriteConfirmModalPage {
constructor(
Expand All @@ -28,7 +29,6 @@
) {}

dialog(): Locator {
/** MUI dialogs may not use the plain title as the whole accessible name across engines. */
return this.page.getByRole('dialog').filter({
has: this.page.getByRole('heading', {
name: this.t['notebook.overwrite.modal.title'],
Expand All @@ -39,16 +39,14 @@

async expectDialogVisible(timeout = 15_000): Promise<void> {
await expect(this.dialog()).toBeVisible({ timeout });
await expect(
this.dialog().getByText(this.t['notebook.overwrite.modal.description'], {
exact: true,
}),
).toBeVisible();
}

async clickCancel(): Promise<void> {
async clickBack(): Promise<void> {
await this.dialog()
.getByRole('button', { name: this.t['common.cancel'], exact: true })
.getByRole('button', {
name: this.t['notebook.overwrite.modal.back'],
exact: true,
})
.click();
}

Expand All @@ -58,12 +56,19 @@
).toBeVisible();
}

async clickOverwrite(): Promise<void> {
uploadButtonPattern(): RegExp {
const escaped = substituteNotebookTemplate(
this.t['notebook.overwrite.modal.action'],
{ count: '\\d+' },

Check warning on line 62 in workspaces/intelligent-assistant/e2e-tests/pages/NotebookOverwriteConfirmModalPage.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`String.raw` should be used to avoid escaping `\`.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ_dUGAa0xRLlS5iFOxL&open=AZ_dUGAa0xRLlS5iFOxL&pullRequest=3849
)
.replace('(', '\\(')

Check warning on line 64 in workspaces/intelligent-assistant/e2e-tests/pages/NotebookOverwriteConfirmModalPage.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`String.raw` should be used to avoid escaping `\`.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ_dUGAa0xRLlS5iFOxM&open=AZ_dUGAa0xRLlS5iFOxM&pullRequest=3849
.replace(')', '\\)');

Check warning on line 65 in workspaces/intelligent-assistant/e2e-tests/pages/NotebookOverwriteConfirmModalPage.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`String.raw` should be used to avoid escaping `\`.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ_dUGAa0xRLlS5iFOxN&open=AZ_dUGAa0xRLlS5iFOxN&pullRequest=3849
return new RegExp(escaped);
}

async clickUpload(): Promise<void> {
await this.dialog()
.getByRole('button', {
name: this.t['notebook.overwrite.modal.action'],
exact: true,
})
.getByRole('button', { name: this.uploadButtonPattern() })
.click();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { expect, type Locator, type Page } from '@playwright/test';

import type { LightspeedMessages } from '../utils/translations';
import { substituteNotebookTemplate } from '../utils/notebookTranslation';
import { openLightspeed } from '../utils/testHelper';

import { NotebookAddDocumentModalPage } from './NotebookAddDocumentModalPage';
Expand Down Expand Up @@ -341,12 +342,12 @@ export class NotebookSurfacePage {
});
}

/**
* Shown on each card as count + plural label (same pattern as NotebookCard.tsx:
* `{ document_count } { t('notebooks.documents') }`, not `notebook.view.documents.count`).
*/
formatNotebookCardDocumentsSummary(documentCount: number): string {
return `${documentCount} ${this.t['notebooks.documents']}`;
if (documentCount === 0) return this.t['notebooks.documents.none'];
if (documentCount === 1) return this.t['notebooks.documents.one'];
return substituteNotebookTemplate(this.t['notebooks.documents.other'], {
count: documentCount,
});
}

async expectUntitledNotebookCardCount(expected: number): Promise<void> {
Expand Down
Loading
Loading