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:
@@ -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) => {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 }} />
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user