fix: clear selection on files & uploads page after deleting (#2056)

* refactor: remove selected rows when deleting or adding elements

* refactor: ensure unique asset IDs when adding new ones

* refactor: remove unnecessary loading checks in mockStore function

* test: add unit tests for TableActions component
This commit is contained in:
Brayan Cerón
2025-06-24 10:19:36 -05:00
committed by GitHub
parent 71fa247c61
commit 60cebf703d
6 changed files with 167 additions and 23 deletions

View File

@@ -70,15 +70,6 @@ const mockStore = async (
}
renderComponent();
await executeThunk(fetchAssets(courseId), store.dispatch);
// Finish loading the expected files into the data table before returning,
// because loading new files can disrupt things like accessing file menus.
if (status === RequestStatus.SUCCESSFUL) {
const numFiles = skipNextPageFetch ? 13 : 15;
await waitFor(() => {
expect(screen.getByText(`Showing ${numFiles} of ${numFiles}`)).toBeInTheDocument();
});
}
};
const emptyMockStore = async (status) => {

View File

@@ -28,7 +28,7 @@ const slice = createSlice({
if (isEmpty(state.assetIds)) {
state.assetIds = payload.assetIds;
} else {
state.assetIds = [...state.assetIds, ...payload.assetIds];
state.assetIds = [...new Set([...state.assetIds, ...payload.assetIds])];
}
},
setSortedAssetIds: (state, { payload }) => {

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
@@ -7,6 +7,7 @@ import {
AlertModal,
Button,
Collapsible,
DataTableContext,
Hyperlink,
Truncate,
} from '@openedx/paragon';
@@ -22,6 +23,13 @@ const DeleteConfirmationModal = ({
// injected
intl,
}) => {
const { clearSelection } = useContext(DataTableContext);
const handleConfirmDeletion = () => {
handleBulkDelete();
clearSelection();
};
const firstSelectedRow = selectedRows[0]?.original;
let activeContentRows = [];
if (Array.isArray(selectedRows)) {
@@ -73,7 +81,7 @@ const DeleteConfirmationModal = ({
<Button variant="tertiary" onClick={closeDeleteConfirmation}>
{intl.formatMessage(messages.cancelButtonLabel)}
</Button>
<Button onClick={handleBulkDelete}>
<Button onClick={handleConfirmDeletion}>
{intl.formatMessage(messages.deleteFileButtonLabel)}
</Button>
</ActionRow>

View File

@@ -273,6 +273,16 @@ const FileTable = ({
setSelectedRows={setSelectedRows}
fileType={fileType}
/>
<DeleteConfirmationModal
{...{
isDeleteConfirmationOpen,
closeDeleteConfirmation,
handleBulkDelete,
selectedRows,
fileType,
}}
/>
</DataTable>
<FileInput key="generic-file-upload" fileInput={fileInputControl} supportedFileFormats={supportedFileFormats} />
{!isEmpty(selectedRows) && (
@@ -286,15 +296,7 @@ const FileTable = ({
sidebar={infoModalSidebar}
/>
)}
<DeleteConfirmationModal
{...{
isDeleteConfirmationOpen,
closeDeleteConfirmation,
handleBulkDelete,
selectedRows,
fileType,
}}
/>
</div>
);
};

View File

@@ -26,13 +26,18 @@ const TableActions = ({
intl,
}) => {
const [isSortOpen, openSort, closeSort] = useToggle(false);
const { state } = useContext(DataTableContext);
const { state, clearSelection } = useContext(DataTableContext);
// This useEffect saves DataTable state so it can persist after table re-renders due to data reload.
useEffect(() => {
setInitialState(state);
}, [state]);
const handleOpenFileSelector = () => {
fileInputControl.click();
clearSelection();
};
return (
<>
<Button variant="outline-primary" onClick={openSort} iconBefore={Tune}>
@@ -71,7 +76,7 @@ const TableActions = ({
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<Button iconBefore={Add} onClick={fileInputControl.click}>
<Button iconBefore={Add} onClick={handleOpenFileSelector}>
{intl.formatMessage(messages.addFilesButtonLabel, { fileType })}
</Button>
<SortAndFilterModal {...{ isSortOpen, closeSort, handleSort }} />

View File

@@ -0,0 +1,138 @@
import React from 'react';
import { screen, fireEvent } from '@testing-library/react';
import { DataTableContext } from '@openedx/paragon';
import { initializeMocks, render } from '../../../testUtils';
import TableActions from './TableActions';
import messages from '../messages';
const defaultProps = {
selectedFlatRows: [],
fileInputControl: { click: jest.fn() },
handleOpenDeleteConfirmation: jest.fn(),
handleBulkDownload: jest.fn(),
encodingsDownloadUrl: null,
handleSort: jest.fn(),
fileType: 'video',
setInitialState: jest.fn(),
intl: {
formatMessage: (msg, values) => msg.defaultMessage.replace('{fileType}', values?.fileType ?? ''),
},
};
const mockColumns = [
{
id: 'wrapperType',
Header: 'Type',
accessor: 'wrapperType',
filter: 'includes',
},
];
const renderWithContext = (props = {}, contextOverrides = {}) => {
const contextValue = {
state: {
selectedRowIds: {},
filters: [],
...contextOverrides.state,
},
clearSelection: jest.fn(),
gotoPage: jest.fn(),
setAllFilters: jest.fn(),
columns: mockColumns,
...contextOverrides,
};
return render(
<DataTableContext.Provider value={contextValue}>
<TableActions {...defaultProps} {...props} />
</DataTableContext.Provider>,
);
};
describe('TableActions', () => {
beforeEach(() => {
initializeMocks();
jest.clearAllMocks();
});
test('renders buttons and dropdown', () => {
renderWithContext();
expect(screen.getByRole('button', { name: messages.sortButtonLabel.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: messages.addFilesButtonLabel.defaultMessage.replace('{fileType}', 'video') })).toBeInTheDocument();
});
test('disables bulk and delete actions if no rows selected', () => {
renderWithContext();
fireEvent.click(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage }));
const downloadOption = screen.getByText(messages.downloadTitle.defaultMessage);
const deleteButton = screen.getByTestId('open-delete-confirmation-button');
expect(downloadOption).toHaveAttribute('aria-disabled', 'true');
expect(downloadOption).toHaveClass('disabled');
expect(deleteButton).toHaveAttribute('aria-disabled', 'true');
expect(deleteButton).toHaveClass('disabled');
});
test('enables bulk and delete actions when rows are selected', () => {
renderWithContext({
selectedFlatRows: [{ original: { id: '1', displayName: 'Video 1', wrapperType: 'video' } }],
});
fireEvent.click(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage }));
expect(screen.getByText(messages.downloadTitle.defaultMessage)).not.toBeDisabled();
expect(screen.getByTestId('open-delete-confirmation-button')).not.toBeDisabled();
});
test('calls file input click and clears selection when add button clicked', () => {
const mockClick = jest.fn();
const mockClear = jest.fn();
renderWithContext({ fileInputControl: { click: mockClick } }, {}, mockClear);
fireEvent.click(screen.getByRole('button', { name: messages.addFilesButtonLabel.defaultMessage.replace('{fileType}', 'video') }));
expect(mockClick).toHaveBeenCalled();
});
test('opens sort modal when sort button clicked', () => {
renderWithContext();
fireEvent.click(screen.getByRole('button', { name: messages.sortButtonLabel.defaultMessage }));
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
test('calls handleBulkDownload when selected and clicked', () => {
const handleBulkDownload = jest.fn();
renderWithContext({
selectedFlatRows: [{ original: { id: '1', displayName: 'Video 1', wrapperType: 'video' } }],
handleBulkDownload,
});
fireEvent.click(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage }));
fireEvent.click(screen.getByText(messages.downloadTitle.defaultMessage));
expect(handleBulkDownload).toHaveBeenCalled();
});
test('calls handleOpenDeleteConfirmation when clicked', () => {
const handleOpenDeleteConfirmation = jest.fn();
const selectedFlatRows = [{ original: { id: '1', displayName: 'Video 1', wrapperType: 'video' } }];
renderWithContext({
selectedFlatRows,
handleOpenDeleteConfirmation,
});
fireEvent.click(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage }));
fireEvent.click(screen.getByTestId('open-delete-confirmation-button'));
expect(handleOpenDeleteConfirmation).toHaveBeenCalledWith(selectedFlatRows);
});
test('shows encoding download link when provided', () => {
const encodingsDownloadUrl = '/some/path/to/encoding.zip';
renderWithContext({ encodingsDownloadUrl });
fireEvent.click(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage }));
expect(screen.getByRole('link', { name: messages.downloadEncodingsTitle.defaultMessage })).toHaveAttribute('href', expect.stringContaining(encodingsDownloadUrl));
});
});