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
9 changes: 0 additions & 9 deletions src/files-and-videos/files-page/FilesPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion src/files-and-videos/files-page/data/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
12 changes: 10 additions & 2 deletions src/files-and-videos/generic/DeleteConfirmationModal.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -7,6 +7,7 @@ import {
AlertModal,
Button,
Collapsible,
DataTableContext,
Hyperlink,
Truncate,
} from '@openedx/paragon';
Expand All @@ -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)) {
Expand Down Expand Up @@ -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>
Expand Down
20 changes: 11 additions & 9 deletions src/files-and-videos/generic/FileTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) && (
Expand All @@ -286,15 +296,7 @@ const FileTable = ({
sidebar={infoModalSidebar}
/>
)}
<DeleteConfirmationModal
{...{
isDeleteConfirmationOpen,
closeDeleteConfirmation,
handleBulkDelete,
selectedRows,
fileType,
}}
/>

</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}>
Expand Down Expand Up @@ -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 }} />
Expand Down
138 changes: 138 additions & 0 deletions src/files-and-videos/generic/table-components/TableActions.test.jsx
Original file line number Diff line number Diff line change
@@ -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));
});
});