Skip to content
Merged
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-lightspeed': patch
---

Improved notebook upload modal and MessageBar UX.
5 changes: 5 additions & 0 deletions workspaces/lightspeed/.changeset/overwrite-flow-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-lightspeed': patch
---

Fixed overwrite flow to add duplicate files to the Add Document modal instead of uploading immediately. Reduced notebook delete toast timeout to 2 seconds.
Original file line number Diff line number Diff line change
Expand Up @@ -230,17 +230,22 @@ export const lightspeedTranslationRef: TranslationRef<
readonly 'notebook.view.upload.heading': string;
readonly 'notebook.view.upload.action': string;
readonly 'notebook.view.input.placeholder': string;
readonly 'notebook.view.input.disabledTooltip': string;
readonly 'notebook.view.sidebar.collapse': string;
readonly 'notebook.view.sidebar.expand': string;
readonly 'notebook.view.sidebar.resize': string;
readonly 'notebook.view.documents.uploading': string;
readonly 'notebook.view.documents.maxReached': string;
readonly 'notebook.upload.success': string;
readonly 'notebook.upload.failed': string;
readonly 'notebook.upload.modal.title': string;
readonly 'notebook.upload.modal.dragDropTitle': string;
readonly 'notebook.upload.modal.browseButton': string;
readonly 'notebook.upload.modal.separator': string;
readonly 'notebook.upload.modal.infoText': string;
readonly 'notebook.upload.modal.selectedFiles': string;
readonly 'notebook.upload.modal.addButton': string;
readonly 'notebook.upload.modal.removeFile': string;
readonly 'notebook.upload.error.unsupportedType': string;
readonly 'notebook.upload.error.fileTooLarge': string;
readonly 'notebook.upload.error.tooManyFiles': string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1483,7 +1483,7 @@ export const LightspeedChat = ({
variant={AlertVariant[variant ?? 'success']}
title={title}
className={classes.toastAlert}
timeout={8000}
timeout={2000}
onTimeout={() => handleRemoveNotebookAlert(key as React.Key)}
actionClose={
<AlertActionCloseButton
Expand Down Expand Up @@ -1681,6 +1681,7 @@ export const LightspeedChat = ({
avatar={avatar}
profileLoading={profileLoading}
topicRestrictionEnabled={topicRestrictionEnabled}
selectedModel={selectedModel}
onClose={handleCloseNotebook}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
/*
* Copyright Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { fireEvent, render, screen, waitFor } from '@testing-library/react';

import { mockUseTranslation } from '../../test-utils/mockTranslations';
import { AddDocumentModal } from '../notebooks/AddDocumentModal';

jest.mock('../../hooks/useTranslation', () => ({
useTranslation: jest.fn(() => mockUseTranslation()),
}));

const mockMutateAsync = jest.fn();
jest.mock('../../hooks/notebooks/useUploadDocument', () => ({
useUploadDocument: () => ({
mutateAsync: mockMutateAsync,
}),
}));

describe('AddDocumentModal', () => {
const defaultProps = {
isOpen: true,
onClose: jest.fn(),
sessionId: 'test-session-id',
existingDocumentNames: [],
onFilesUploading: jest.fn(),
onUploadStarted: jest.fn(),
onUploadFailed: jest.fn(),
onDuplicatesFound: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
mockMutateAsync.mockResolvedValue({ document_id: 'test-doc-id' });
});

it('should render the modal when open', () => {
render(<AddDocumentModal {...defaultProps} />);

expect(screen.getByText('Add a document to Notebook')).toBeInTheDocument();
expect(screen.getByText('Drag and drop files here')).toBeInTheDocument();
});

it('should not render when isOpen is false', () => {
render(<AddDocumentModal {...defaultProps} isOpen={false} />);

expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});

it('should render Cancel and Add buttons', () => {
render(<AddDocumentModal {...defaultProps} />);

expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Add (0)' })).toBeInTheDocument();
});

it('should have Add button disabled when no files selected', () => {
render(<AddDocumentModal {...defaultProps} />);

const addButton = screen.getByRole('button', { name: 'Add (0)' });
expect(addButton).toBeDisabled();
});

it('should call onClose when Cancel button is clicked', () => {
render(<AddDocumentModal {...defaultProps} />);

const cancelButton = screen.getByRole('button', { name: 'Cancel' });
fireEvent.click(cancelButton);

expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
});

it('should call onClose when close icon is clicked', () => {
render(<AddDocumentModal {...defaultProps} />);

const closeButton = screen.getByRole('button', { name: 'Close' });
fireEvent.click(closeButton);

expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
});

it('should display file list when files are dropped', async () => {
render(<AddDocumentModal {...defaultProps} />);

const dropzone = screen
.getByText('Drag and drop files here')
.closest('div');
const file = new File(['content'], 'test-file.txt', { type: 'text/plain' });

fireEvent.drop(dropzone!, {

Check warning on line 103 in workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AddDocumentModal.test.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since the receiver accepts the original type of the expression.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ3S2ey2MYVOBW_DrkJB&open=AZ3S2ey2MYVOBW_DrkJB&pullRequest=2936
dataTransfer: {
files: [file],
types: ['Files'],
},
});

await waitFor(() => {
expect(screen.getByText('test-file.txt')).toBeInTheDocument();
});
});

it('should update Add button count when files are selected', async () => {
render(<AddDocumentModal {...defaultProps} />);

const dropzone = screen
.getByText('Drag and drop files here')
.closest('div');
const file = new File(['content'], 'test-file.txt', { type: 'text/plain' });

fireEvent.drop(dropzone!, {

Check warning on line 123 in workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AddDocumentModal.test.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since the receiver accepts the original type of the expression.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ3S2ey2MYVOBW_DrkJC&open=AZ3S2ey2MYVOBW_DrkJC&pullRequest=2936
dataTransfer: {
files: [file],
types: ['Files'],
},
});

await waitFor(() => {
expect(
screen.getByRole('button', { name: 'Add (1)' }),
).toBeInTheDocument();
});
});

it('should not auto-close modal after file drop', async () => {
render(<AddDocumentModal {...defaultProps} />);

const dropzone = screen
.getByText('Drag and drop files here')
.closest('div');
const file = new File(['content'], 'test-file.txt', { type: 'text/plain' });

fireEvent.drop(dropzone!, {

Check warning on line 145 in workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AddDocumentModal.test.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since the receiver accepts the original type of the expression.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ3S2ey2MYVOBW_DrkJD&open=AZ3S2ey2MYVOBW_DrkJD&pullRequest=2936
dataTransfer: {
files: [file],
types: ['Files'],
},
});

await waitFor(() => {
expect(screen.getByText('test-file.txt')).toBeInTheDocument();
});

expect(defaultProps.onClose).not.toHaveBeenCalled();
});

it('should trigger upload and close when Add button is clicked', async () => {
render(<AddDocumentModal {...defaultProps} />);

const dropzone = screen
.getByText('Drag and drop files here')
.closest('div');
const file = new File(['content'], 'test-file.txt', { type: 'text/plain' });

fireEvent.drop(dropzone!, {

Check warning on line 167 in workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AddDocumentModal.test.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since the receiver accepts the original type of the expression.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ3S2ey2MYVOBW_DrkJE&open=AZ3S2ey2MYVOBW_DrkJE&pullRequest=2936
dataTransfer: {
files: [file],
types: ['Files'],
},
});

await waitFor(() => {
expect(
screen.getByRole('button', { name: 'Add (1)' }),
).not.toBeDisabled();
});

fireEvent.click(screen.getByRole('button', { name: 'Add (1)' }));

await waitFor(() => {
expect(defaultProps.onFilesUploading).toHaveBeenCalledWith([file]);
expect(mockMutateAsync).toHaveBeenCalledWith({
sessionId: 'test-session-id',
file,
});
expect(defaultProps.onClose).toHaveBeenCalled();
});
});

it('should allow removing files from the list', async () => {
render(<AddDocumentModal {...defaultProps} />);

const dropzone = screen
.getByText('Drag and drop files here')
.closest('div');
const file = new File(['content'], 'test-file.txt', { type: 'text/plain' });

fireEvent.drop(dropzone!, {

Check warning on line 200 in workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AddDocumentModal.test.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since the receiver accepts the original type of the expression.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ3S2ey2MYVOBW_DrkJF&open=AZ3S2ey2MYVOBW_DrkJF&pullRequest=2936
dataTransfer: {
files: [file],
types: ['Files'],
},
});

await waitFor(() => {
expect(screen.getByText('test-file.txt')).toBeInTheDocument();
});

const removeButton = screen.getByRole('button', {
name: 'Remove test-file.txt',
});
fireEvent.click(removeButton);

await waitFor(() => {
expect(screen.queryByText('test-file.txt')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Add (0)' })).toBeDisabled();
});
});

it('should clear selected files when modal is closed', async () => {
const { rerender } = render(<AddDocumentModal {...defaultProps} />);

const dropzone = screen
.getByText('Drag and drop files here')
.closest('div');
const file = new File(['content'], 'test-file.txt', { type: 'text/plain' });

fireEvent.drop(dropzone!, {

Check warning on line 230 in workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AddDocumentModal.test.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since the receiver accepts the original type of the expression.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ3S2ey2MYVOBW_DrkJG&open=AZ3S2ey2MYVOBW_DrkJG&pullRequest=2936
dataTransfer: {
files: [file],
types: ['Files'],
},
});

await waitFor(() => {
expect(screen.getByText('test-file.txt')).toBeInTheDocument();
});

fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));

rerender(<AddDocumentModal {...defaultProps} isOpen />);

expect(screen.queryByText('test-file.txt')).not.toBeInTheDocument();
});

it('should call onDuplicatesFound for files that already exist', async () => {
render(
<AddDocumentModal
{...defaultProps}
existingDocumentNames={['existing-file.txt']}
/>,
);

const dropzone = screen
.getByText('Drag and drop files here')
.closest('div');
const existingFile = new File(['content'], 'existing-file.txt', {
type: 'text/plain',
});
const newFile = new File(['content'], 'new-file.txt', {
type: 'text/plain',
});

fireEvent.drop(dropzone!, {

Check warning on line 266 in workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AddDocumentModal.test.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since the receiver accepts the original type of the expression.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ3S2ey2MYVOBW_DrkJH&open=AZ3S2ey2MYVOBW_DrkJH&pullRequest=2936
dataTransfer: {
files: [existingFile, newFile],
types: ['Files'],
},
});

await waitFor(() => {
expect(defaultProps.onDuplicatesFound).toHaveBeenCalledWith([
existingFile,
]);
expect(screen.getByText('new-file.txt')).toBeInTheDocument();
expect(screen.queryByText('existing-file.txt')).not.toBeInTheDocument();
});
});
});
Loading
Loading