fix: combine filter and sort into one modal (#680)

This commit is contained in:
Kristin Aoki
2023-11-13 15:22:44 -05:00
committed by GitHub
parent 2fbb490cbb
commit 3378c8e170
22 changed files with 1005 additions and 178 deletions

View File

@@ -1,5 +1,6 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { isEmpty } from 'lodash';
import { useDispatch, useSelector } from 'react-redux';
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
import { CheckboxFilter } from '@edx/paragon';
@@ -85,12 +86,24 @@ const FilesPage = ({
const activeColumn = {
id: 'usageLocations',
Header: 'Active',
accessor: (({ usageLocations }) => !isEmpty(usageLocations)),
Cell: ({ row }) => ActiveColumn({ row }),
Filter: CheckboxFilter,
filterChoices: [
{ name: intl.formatMessage(messages.activeCheckboxLabel), value: true },
{ name: intl.formatMessage(messages.inactiveCheckboxLabel), value: false },
],
};
const accessColumn = {
id: 'locked',
Header: 'Access',
accessor: 'locked',
Cell: ({ row }) => AccessColumn({ row }),
Filter: CheckboxFilter,
filterChoices: [
{ name: intl.formatMessage(messages.lockedCheckboxLabel), value: true },
{ name: intl.formatMessage(messages.publicCheckboxLabel), value: false },
],
};
const thumbnailColumn = {
id: 'thumbnail',
@@ -100,6 +113,7 @@ const FilesPage = ({
const fileSizeColumn = {
id: 'fileSize',
Header: 'File size',
accessor: 'fileSize',
Cell: ({ row }) => {
const { fileSize } = row.original;
return getFileSizeToClosestByte(fileSize);
@@ -120,21 +134,25 @@ const FilesPage = ({
filter: 'includesValue',
filterChoices: [
{
name: 'Code',
name: intl.formatMessage(messages.codeCheckboxLabel),
value: 'code',
},
{
name: 'Images',
name: intl.formatMessage(messages.imageCheckboxLabel),
value: 'image',
},
{
name: 'Documents',
name: intl.formatMessage(messages.documentCheckboxLabel),
value: 'document',
},
{
name: 'Audio',
name: intl.formatMessage(messages.audioCheckboxLabel),
value: 'audio',
},
{
name: intl.formatMessage(messages.otherCheckboxLabel),
value: 'other',
},
],
},
{ ...activeColumn },

View File

@@ -45,6 +45,42 @@ const messages = defineMessages({
you lock a file, the web URL only allows learners who are enrolled
in your course and signed in to access the file.`,
},
activeCheckboxLabel: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.activeCheckbox.label',
defaultMessage: 'Active',
},
inactiveCheckboxLabel: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.inactiveCheckbox.label',
defaultMessage: 'Inactive',
},
lockedCheckboxLabel: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.lockedCheckbox.label',
defaultMessage: 'Locked',
},
publicCheckboxLabel: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.publicCheckbox.label',
defaultMessage: 'Public',
},
codeCheckboxLabel: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.codeCheckbox.label',
defaultMessage: 'Code',
},
imageCheckboxLabel: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.imageCheckbox.label',
defaultMessage: 'Images',
},
documentCheckboxLabel: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.documentCheckbox.label',
defaultMessage: 'Documents',
},
audioCheckboxLabel: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.audioCheckbox.label',
defaultMessage: 'Audio',
},
otherCheckboxLabel: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.otherCheckbox.label',
defaultMessage: 'Other',
},
});
export default messages;

View File

@@ -22,10 +22,11 @@ import FileInput, { useFileInput } from './FileInput';
import {
GalleryCard,
TableActions,
RowStatus,
MoreInfoColumn,
FilterStatus,
} from './table-components';
import ApiStatusToast from './ApiStatusToast';
import FilterStatus from './table-components/FilterStatus';
import MoreInfoColumn from './table-components/table-custom-columns/MoreInfoColumn';
const FileTable = ({
files,
@@ -171,7 +172,7 @@ const FileTable = ({
}
return (
<>
<div className="files-table">
<DataTable
isFilterable
isLoading={loadingStatus === RequestStatus.IN_PROGRESS}
@@ -195,6 +196,7 @@ const FileTable = ({
pageCount={pageCount}
data={files}
FilterStatusComponent={FilterStatus}
RowStatusComponent={RowStatus}
>
{isEmpty(files) && loadingStatus !== RequestStatus.IN_PROGRESS ? (
<Dropzone
@@ -261,7 +263,7 @@ const FileTable = ({
>
{intl.formatMessage(messages.deleteConfirmationMessage, { fileNumber: selectedRows.length })}
</AlertModal>
</>
</div>
);
};

View File

@@ -1,6 +1,10 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
rowStatusMessage: {
id: 'course-authoring.files-and-upload.rowStatus.message',
defaultMessage: 'Showing {fileCount} of {rowCount}',
},
apiStatusToastMessage: {
id: 'course-authoring.files-and-upload.apiStatus.message',
defaultMessage: '{actionType} {selectedRowCount} file(s)',
@@ -95,7 +99,7 @@ const messages = defineMessages({
},
sortButtonLabel: {
id: 'course-authoring.files-and-uploads.sortButton.label',
defaultMessage: 'Sort',
defaultMessage: 'Sort and Filter',
},
sortModalTitleLabel: {
id: 'course-authoring.files-and-uploads.sortModal.title',

View File

@@ -1,46 +1,56 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { DataTableContext, Button } from '@edx/paragon';
import {
DataTableContext, Button, Row, Chip,
} from '@edx/paragon';
import { Close } from '@edx/paragon/icons';
import { getFilters, removeFilter } from './utils';
const FilterStatus = ({
className, variant, size, clearFiltersText, buttonClassName,
}) => {
const {
setAllFilters, RowStatusComponent, page, rows,
state, setAllFilters, setFilter, RowStatusComponent, columns,
} = useContext(DataTableContext);
if (!setAllFilters) {
return null;
}
const RowStatus = RowStatusComponent;
const pageSize = page?.length || rows?.length;
const filters = getFilters(state, columns);
return (
<div className={className}>
<div className="pl-1">
<span>Filters applied</span>
{!!pageSize && ' ('}
<RowStatus className="d-inline" />
{!!pageSize && ')'}
</div>
<Button
className={buttonClassName}
variant={variant}
size={size}
onClick={() => setAllFilters([])}
>
{clearFiltersText === undefined
? (
<FormattedMessage
id="pgn.DataTable.FilterStatus.clearFiltersText"
defaultMessage="Clear filters"
description="A text that appears on the `Clear filters` button"
/>
)
: clearFiltersText}
</Button>
<RowStatusComponent />
<Row className="m-0 align-items-center">
<span className="mr-2">Filters applied</span>
{filters.map(({ name, value }) => (
<Chip
key={value}
iconAfter={Close}
onIconAfterClick={() => removeFilter(value, setFilter, setAllFilters, state)}
>
{name}
</Chip>
))}
<Button
className={buttonClassName}
variant={variant}
size={size}
onClick={() => setAllFilters([])}
>
{clearFiltersText === undefined
? (
<FormattedMessage
id="pgn.DataTable.FilterStatus.clearFiltersText"
defaultMessage="Clear filters"
description="A text that appears on the `Clear filters` button"
/>
)
: clearFiltersText}
</Button>
</Row>
</div>
);
};

View File

@@ -0,0 +1,30 @@
import React, { useContext } from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { DataTableContext } from '@edx/paragon';
import { getCurrentViewRange } from './utils';
const RowStatus = ({
// injected
intl,
}) => {
const { filteredRows, page, initialRows } = useContext(DataTableContext);
return (
<div>
<span>
{getCurrentViewRange({
filterRowCount: filteredRows.length,
initialRowCount: initialRows.length,
fileCount: page.length,
intl,
})}
</span>
</div>
);
};
RowStatus.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(RowStatus);

View File

@@ -1,18 +1,16 @@
import React, { useState } from 'react';
import React from 'react';
import _ from 'lodash';
import { PropTypes } from 'prop-types';
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
import { injectIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import {
ActionRow,
Button,
Dropdown,
ModalDialog,
SelectableBox,
useToggle,
} from '@edx/paragon';
import { Add } from '@edx/paragon/icons';
import { Add, Tune } from '@edx/paragon/icons';
import messages from '../messages';
import SortAndFilterModal from './sort-and-filter-modal';
const TableActions = ({
selectedFlatRows,
@@ -21,17 +19,11 @@ const TableActions = ({
handleBulkDownload,
handleOpenDeleteConfirmation,
encodingsDownloadUrl,
// injected
intl,
}) => {
const [isSortOpen, openSort, closeSort] = useToggle(false);
const [sortBy, setSortBy] = useState('dateAdded,desc');
const handleChange = (e) => {
setSortBy(e.target.value);
};
return (
<>
<Button variant="outline-primary" onClick={openSort}>
<Button variant="outline-primary" onClick={openSort} iconBefore={Tune}>
<FormattedMessage {...messages.sortButtonLabel} />
</Button>
<Dropdown className="mx-2">
@@ -70,94 +62,7 @@ const TableActions = ({
<Button iconBefore={Add} onClick={fileInputControl.click}>
<FormattedMessage {...messages.addFilesButtonLabel} />
</Button>
<ModalDialog
title={intl.formatMessage(messages.sortModalTitleLabel)}
isOpen={isSortOpen}
onClose={closeSort}
size="lg"
hasCloseButton
>
<ModalDialog.Header>
<ModalDialog.Title>
<FormattedMessage {...messages.sortModalTitleLabel} />
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<SelectableBox.Set
type="radio"
value={sortBy}
onChange={handleChange}
name="sort options"
columns={3}
ariaLabel="sort by selection"
>
<SelectableBox
className="text-center"
value="displayName,asc"
type="radio"
aria-label="name descending radio"
>
<FormattedMessage {...messages.sortByNameAscending} />
</SelectableBox>
<SelectableBox
className="text-center"
value="dateAdded,desc"
type="radio"
aria-label="date added descending radio"
>
<FormattedMessage {...messages.sortByNewest} />
</SelectableBox>
<SelectableBox
className="text-center"
value="fileSize,desc"
type="radio"
aria-label="date added descending radio"
>
<FormattedMessage {...messages.sortBySizeDescending} />
</SelectableBox>
<SelectableBox
className="text-center"
value="displayName,desc"
type="radio"
aria-label="name ascending radio"
>
<FormattedMessage {...messages.sortByNameDescending} />
</SelectableBox>
<SelectableBox
className="text-center"
value="dateAdded,asc"
type="radio"
aria-label="date added ascending radio"
>
<FormattedMessage {...messages.sortByOldest} />
</SelectableBox>
<SelectableBox
className="text-center"
value="fileSize,asc"
type="radio"
aria-label="date added ascending radio"
>
<FormattedMessage {...messages.sortBySizeAscending} />
</SelectableBox>
</SelectableBox.Set>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
<FormattedMessage {...messages.cancelButtonLabel} />
</ModalDialog.CloseButton>
<Button
variant="primary"
onClick={() => {
closeSort();
handleSort(sortBy);
}}
>
<FormattedMessage {...messages.applySortButton} />
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
<SortAndFilterModal {...{ isSortOpen, closeSort, handleSort }} />
</>
);
};
@@ -186,8 +91,6 @@ TableActions.propTypes = {
handleBulkDownload: PropTypes.func.isRequired,
encodingsDownloadUrl: PropTypes.string,
handleSort: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
};
TableActions.defaultProps = {

View File

@@ -1,5 +1,7 @@
import GalleryCard from './GalleryCard';
import TableActions from './TableActions';
import FilterStatus from './FilterStatus';
import RowStatus from './RowStatus';
import {
AccessColumn,
ActiveColumn,
@@ -11,6 +13,8 @@ import {
export {
TableActions,
GalleryCard,
FilterStatus,
RowStatus,
AccessColumn,
ActiveColumn,
MoreInfoColumn,

View File

@@ -0,0 +1,180 @@
import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import {
ActionRow,
Button,
DataTableContext,
Form,
ModalDialog,
SelectableBox,
useCheckboxSetValues,
} from '@edx/paragon';
import messages from './messages';
import { getCheckedFilters, getFilterOptions, processFilters } from './utils';
const SortAndFilterModal = ({
isSortOpen,
closeSort,
handleSort,
// injected
intl,
}) => {
const { state, setAllFilters, columns } = useContext(DataTableContext);
const filterOptions = getFilterOptions(columns);
const currentFilters = getCheckedFilters(state);
const [sortBy, setSortBy] = useState('dateAdded,desc');
const [filterBy, {
add, remove, set, clear,
}] = useCheckboxSetValues(currentFilters);
useEffect(() => {
const updatedFilters = getCheckedFilters(state);
set(updatedFilters);
}, [state]);
const handleChange = (e) => {
setSortBy(e.target.value);
};
const handleFilterUpdate = (e) => {
if (e.target.checked) {
add(e.target.value);
} else {
remove(e.target.value);
}
};
const handleApply = () => {
closeSort();
handleSort(sortBy);
processFilters(filterBy, columns, setAllFilters);
};
const handleClearAll = () => {
setSortBy('dateAdded,desc');
clear();
};
return (
<ModalDialog
title={intl.formatMessage(messages.modalTitle)}
isOpen={isSortOpen}
onClose={closeSort}
size="lg"
hasCloseButton
>
<ModalDialog.Header>
<ModalDialog.Title>
<FormattedMessage {...messages.modalTitle} />
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<div className="h4 mb-4">
<FormattedMessage {...messages.sortByHeader} />
</div>
<SelectableBox.Set
type="radio"
value={sortBy}
onChange={handleChange}
name="sort options"
columns={3}
ariaLabel="sort by selection"
className="mb-4.5"
>
<SelectableBox
className="text-center"
value="displayName,asc"
type="radio"
aria-label="name descending radio"
>
<FormattedMessage {...messages.sortByNameAscending} />
</SelectableBox>
<SelectableBox
className="text-center"
value="dateAdded,desc"
type="radio"
aria-label="date added descending radio"
>
<FormattedMessage {...messages.sortByNewest} />
</SelectableBox>
<SelectableBox
className="text-center"
value="fileSize,desc"
type="radio"
aria-label="file size descending radio"
>
<FormattedMessage {...messages.sortBySizeDescending} />
</SelectableBox>
<SelectableBox
className="text-center"
value="displayName,desc"
type="radio"
aria-label="name ascending radio"
>
<FormattedMessage {...messages.sortByNameDescending} />
</SelectableBox>
<SelectableBox
className="text-center"
value="dateAdded,asc"
type="radio"
aria-label="date added ascending radio"
>
<FormattedMessage {...messages.sortByOldest} />
</SelectableBox>
<SelectableBox
className="text-center"
value="fileSize,asc"
type="radio"
aria-label="file size ascending radio"
>
<FormattedMessage {...messages.sortBySizeAscending} />
</SelectableBox>
</SelectableBox.Set>
<hr />
<div className="h4 my-4">
<FormattedMessage {...messages.filterByHeader} />
</div>
<Form.Group>
<Form.CheckboxSet
name="filters"
onChange={handleFilterUpdate}
value={filterBy}
isInline
>
{filterOptions.map(({ name, value }) => (
<Form.Checkbox {...{ value, key: value }}>{name}</Form.Checkbox>
))}
</Form.CheckboxSet>
</Form.Group>
<Button className="pl-0" variant="link" onClick={handleClearAll}>
<FormattedMessage {...messages.clearAllButtonLabel} />
</Button>
<hr />
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
<FormattedMessage {...messages.cancelButtonLabel} />
</ModalDialog.CloseButton>
<Button
variant="primary"
onClick={handleApply}
>
<FormattedMessage {...messages.applySortButton} />
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
);
};
SortAndFilterModal.propTypes = {
handleSort: PropTypes.func.isRequired,
isSortOpen: PropTypes.bool.isRequired,
closeSort: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
};
export default injectIntl(SortAndFilterModal);

View File

@@ -0,0 +1,3 @@
import SortAndFilterModal from './SortAndFilterModal';
export default SortAndFilterModal;

View File

@@ -0,0 +1,54 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
modalTitle: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.title',
defaultMessage: 'Sort and Filter',
},
sortByHeader: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.sortBySection.header',
defaultMessage: 'Sort by',
},
filterByHeader: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filterBySection.header',
defaultMessage: 'Filter by',
},
clearAllButtonLabel: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.clearAllButton.label',
defaultMessage: 'Clear all',
},
cancelButtonLabel: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.cancelButton.label',
defaultMessage: 'Cancel',
},
sortByNameAscending: {
id: 'course-authoring..files-and-videos.sort-and-filter.modal.sortByNameAscendingButton.label',
defaultMessage: 'Name (A-Z)',
},
sortByNewest: {
id: 'course-authoring..files-and-videos.sort-and-filter.modal.sortByNewestButton.label',
defaultMessage: 'Newest',
},
sortBySizeDescending: {
id: 'course-authoring..files-and-videos.sort-and-filter.modal.sortBySizeDescendingButton.label',
defaultMessage: 'File size (High to low)',
},
sortByNameDescending: {
id: 'course-authoring..files-and-videos.sort-and-filter.modal.sortByNameDescendingButton.label',
defaultMessage: 'Name (Z-A)',
},
sortByOldest: {
id: 'course-authoring..files-and-videos.sort-and-filter.modal.sortByOldestButton.label',
defaultMessage: 'Oldest',
},
sortBySizeAscending: {
id: 'course-authoring..files-and-videos.sort-and-filter.modal.sortBySizeAscendingButton.label',
defaultMessage: 'File size (Low to high)',
},
applySortButton: {
id: 'course-authoring..files-and-videos.sort-and-filter.modal.applyySortButton.label',
defaultMessage: 'Apply',
},
});
export default messages;

View File

@@ -0,0 +1,119 @@
import { isEmpty } from 'lodash';
export const getFilterOptions = (columns) => {
const allOptions = [];
const filterableColumns = columns.filter(column => column?.filterChoices);
filterableColumns.forEach(column => {
const { id, filterChoices } = column;
let updatedChoices = filterChoices;
switch (id) {
case 'locked':
updatedChoices = filterChoices.map(choice => (
{ ...choice, value: choice.value ? 'locked' : 'public' }
));
break;
case 'usageLocations':
updatedChoices = filterChoices.map(choice => (
{ ...choice, value: choice.value ? 'active' : 'inactive' }
));
break;
case 'transcripts':
updatedChoices = filterChoices.map(choice => (
{ ...choice, value: choice.value ? 'transcribed' : 'notTranscribed' }
));
break;
default:
break;
}
allOptions.push(...updatedChoices);
});
return allOptions;
};
export const getCheckedFilters = (state) => {
const { filters } = state;
const allFilters = [];
filters.forEach(filter => {
const { id, value } = filter;
let updatedValues = value;
switch (id) {
case 'locked':
updatedValues = value.map(val => (val ? 'locked' : 'public'));
break;
case 'usageLocations':
updatedValues = value.map(val => (val ? 'active' : 'inactive'));
break;
case 'transcripts':
updatedValues = value.map(val => (val ? 'transcribed' : 'notTranscribed'));
break;
default:
break;
}
allFilters.push(...updatedValues);
});
return allFilters;
};
export const processFilters = (filters, columns, setAllFilters) => {
const filterableColumns = columns.filter(column => column?.filterChoices);
const allFilters = [];
filterableColumns.forEach(({ id, filterChoices }) => {
const filterValues = filterChoices.map(choice => choice.value);
let processedFilters = filters;
switch (id) {
case 'locked':
processedFilters = filters.map(match => {
if (match === 'locked') {
return true;
}
if (match === 'public') {
return false;
}
return match;
});
break;
case 'usageLocations':
processedFilters = filters.map(match => {
if (match === 'active') {
return true;
}
if (match === 'inactive') {
return false;
}
return match;
});
break;
case 'transcripts':
processedFilters = filters.map(match => {
if (match === 'transcribed') {
return true;
}
if (match === 'notTranscribed') {
return false;
}
return match;
});
break;
default:
break;
}
const matchingFilters = filterValues.filter(value => processedFilters.includes(value));
if (!isEmpty(matchingFilters)) {
allFilters.push({ id, value: matchingFilters });
}
});
setAllFilters(allFilters);
};

View File

@@ -0,0 +1,304 @@
import { getCheckedFilters, getFilterOptions, processFilters } from './utils';
describe('getCheckboxFilters', () => {
describe('switch case locked', () => {
it('should equal array with string locked', () => {
const state = {
filters: [
{ id: 'locked', value: [true] },
],
};
const expected = ['locked'];
const actual = getCheckedFilters(state);
expect(actual).toEqual(expected);
});
it('value attribute should equal public', () => {
const state = {
filters: [
{ id: 'locked', value: [false] },
],
};
const expected = ['public'];
const actual = getCheckedFilters(state);
expect(actual).toEqual(expected);
});
});
describe('switch case usageLocations', () => {
it('value attribute should equal active', () => {
const state = {
filters: [
{ id: 'usageLocations', value: [true] },
],
};
const expected = ['active'];
const actual = getCheckedFilters(state);
expect(actual).toEqual(expected);
});
it('value attribute should equal inactive', () => {
const state = {
filters: [
{ id: 'usageLocations', value: [false] },
],
};
const expected = ['inactive'];
const actual = getCheckedFilters(state);
expect(actual).toEqual(expected);
});
});
describe('switch case transcripts', () => {
it('should equal array with string transcribed', () => {
const state = {
filters: [
{ id: 'transcripts', value: [true] },
],
};
const expected = ['transcribed'];
const actual = getCheckedFilters(state);
expect(actual).toEqual(expected);
});
it('should equal array with string notTranscribed', () => {
const state = {
filters: [
{ id: 'transcripts', value: [false] },
],
};
const expected = ['notTranscribed'];
const actual = getCheckedFilters(state);
expect(actual).toEqual(expected);
});
});
describe('switch case default', () => {
it('should equal array with string test', () => {
const state = {
filters: [
{ id: 'testId', value: ['testValue'] },
],
};
const expected = ['testValue'];
const actual = getCheckedFilters(state);
expect(actual).toEqual(expected);
});
});
});
describe('getFilterOptions', () => {
describe('switch case locked', () => {
it('value attribute should equal locked', () => {
const columns = [
{ id: 'locked', filterChoices: [{ name: 'Locked', value: true }] },
];
const expected = [
{ name: 'Locked', value: 'locked' },
];
const actual = getFilterOptions(columns);
expect(actual).toEqual(expected);
});
it('value attribute should equal public', () => {
const columns = [
{ id: 'locked', filterChoices: [{ name: 'Public', value: false }] },
];
const expected = [
{ name: 'Public', value: 'public' },
];
const actual = getFilterOptions(columns);
expect(actual).toEqual(expected);
});
});
describe('switch case usageLocation', () => {
it('value attribute should equal active', () => {
const columns = [
{ id: 'usageLocations', filterChoices: [{ name: 'Active', value: true }] },
];
const expected = [
{ name: 'Active', value: 'active' },
];
const actual = getFilterOptions(columns);
expect(actual).toEqual(expected);
});
it('value attribute should equal inactive', () => {
const columns = [
{ id: 'usageLocations', filterChoices: [{ name: 'Inactive', value: false }] },
];
const expected = [
{ name: 'Inactive', value: 'inactive' },
];
const actual = getFilterOptions(columns);
expect(actual).toEqual(expected);
});
});
describe('switch case transcripts', () => {
it('value attribute should equal transcribed', () => {
const columns = [
{ id: 'transcripts', filterChoices: [{ name: 'Transcribed', value: true }] },
];
const expected = [
{ name: 'Transcribed', value: 'transcribed' },
];
const actual = getFilterOptions(columns);
expect(actual).toEqual(expected);
});
it('value attribute should equal notTranscribed', () => {
const columns = [
{ id: 'transcripts', filterChoices: [{ name: 'Not transcribed', value: false }] },
];
const expected = [
{ name: 'Not transcribed', value: 'notTranscribed' },
];
const actual = getFilterOptions(columns);
expect(actual).toEqual(expected);
});
});
describe('switch case default', () => {
it('value attribute should equal test', () => {
const columns = [
{ id: 'other', filterChoices: [{ name: 'Test', value: 'test' }] },
];
const expected = [
{ name: 'Test', value: 'test' },
];
const actual = getFilterOptions(columns);
expect(actual).toEqual(expected);
});
});
});
describe('processFilters', () => {
const setAllFilters = jest.fn();
beforeEach(() => {
jest.resetAllMocks();
});
it('should call setAllFilters with an empty array', () => {
const filters = [];
const columns = [
{ id: 'locked', filterChoices: [{ name: 'Locked', value: true }] },
];
const expectedParameter = [];
processFilters(filters, columns, setAllFilters);
expect(setAllFilters).toHaveBeenCalledWith(expectedParameter);
});
describe('switch case locked', () => {
it('should call setAllFilters with locked filter', () => {
const filters = ['locked'];
const columns = [
{ id: 'locked', filterChoices: [{ name: 'Locked', value: true }, { name: 'Public', value: false }] },
];
const expectedParameter = [{ id: 'locked', value: [true] }];
processFilters(filters, columns, setAllFilters);
expect(setAllFilters).toHaveBeenCalledWith(expectedParameter);
});
it('should call setAllFilters with public filter', () => {
const filters = ['public', 'filter'];
const columns = [
{ id: 'locked', filterChoices: [{ name: 'Public', value: false }] },
{ id: 'test', filterChoices: [{ name: 'Filter', value: 'filter' }] },
];
const expectedParameter = [
{ id: 'locked', value: [false] },
{ id: 'test', value: ['filter'] },
];
processFilters(filters, columns, setAllFilters);
expect(setAllFilters).toHaveBeenCalledWith(expectedParameter);
});
});
describe('switch case usageLocations', () => {
it('should call setAllFilters with active filter', () => {
const filters = ['active'];
const columns = [
{ id: 'usageLocations', filterChoices: [{ name: 'Active', value: true }] },
];
const expectedParameter = [{ id: 'usageLocations', value: [true] }];
processFilters(filters, columns, setAllFilters);
expect(setAllFilters).toHaveBeenCalledWith(expectedParameter);
});
it('should call setAllFilters with inactive filter', () => {
const filters = ['inactive', 'filter'];
const columns = [
{ id: 'usageLocations', filterChoices: [{ name: 'Inactive', value: false }] },
{ id: 'test', filterChoices: [{ name: 'Filter', value: 'filter' }] },
];
const expectedParameter = [
{ id: 'usageLocations', value: [false] },
{ id: 'test', value: ['filter'] },
];
processFilters(filters, columns, setAllFilters);
expect(setAllFilters).toHaveBeenCalledWith(expectedParameter);
});
});
describe('switch case transcripts', () => {
it('should call setAllFilters with transcribed filter', () => {
const filters = ['transcribed', 'filter'];
const columns = [
{ id: 'transcripts', filterChoices: [{ name: 'Transcribed', value: true }] },
{ id: 'test', filterChoices: [{ name: 'Filter', value: 'filter' }] },
];
const expectedParameter = [
{ id: 'transcripts', value: [true] },
{ id: 'test', value: ['filter'] },
];
processFilters(filters, columns, setAllFilters);
expect(setAllFilters).toHaveBeenCalledWith(expectedParameter);
});
it('should call setAllFilters with notTranscribed filter', () => {
const filters = ['notTranscribed'];
const columns = [
{ id: 'transcripts', filterChoices: [{ name: 'Not transcribed', value: false }] },
];
const expectedParameter = [{ id: 'transcripts', value: [false] }];
processFilters(filters, columns, setAllFilters);
expect(setAllFilters).toHaveBeenCalledWith(expectedParameter);
});
});
describe('switch case default', () => {
it('should call setAllFilters with test filter', () => {
const filters = ['filter'];
const columns = [
{ id: 'test', filterChoices: [{ name: 'Filter', value: 'filter' }] },
];
const expectedParameter = [{ id: 'test', value: ['filter'] }];
processFilters(filters, columns, setAllFilters);
expect(setAllFilters).toHaveBeenCalledWith(expectedParameter);
});
});
});

View File

@@ -4,6 +4,12 @@ import { Badge } from '@edx/paragon';
const StatusColumn = ({ row }) => {
const { status } = row.original;
const isUploaded = status === 'Success';
if (isUploaded) {
return null;
}
return (
<Badge variant="light">
{status}

View File

@@ -0,0 +1,55 @@
import { isEmpty } from 'lodash';
import messages from '../messages';
const getFilterDisplayName = (column, values) => {
const displayNames = [];
const { filterChoices } = column;
values.forEach(value => {
const [displayName] = filterChoices.filter(choice => choice.value === value);
displayNames.push(displayName);
});
return displayNames;
};
export const getFilters = (state, columns) => {
const { filters } = state;
const filterableColumns = columns.filter(column => column?.filterChoices);
const allFilters = [];
filters.forEach(filter => {
const { id, value } = filter;
const [filterColumn] = filterableColumns.filter(column => column.id === id);
const currentFilters = getFilterDisplayName(filterColumn, value);
allFilters.push(...currentFilters);
});
return allFilters;
};
export const removeFilter = (filter, setFilter, setAllFilters, state) => {
const { filters } = state;
const [editedFilter] = filters.filter(currentFilter => currentFilter.value.includes(filter));
const updatedFilterValue = editedFilter.value.filter(value => value !== filter);
if (isEmpty(updatedFilterValue)) {
const updatedFilters = filters.filter(currentFilter => currentFilter.id !== editedFilter.id);
setAllFilters(updatedFilters);
} else {
setFilter(editedFilter.id, updatedFilterValue);
}
};
export const getCurrentViewRange = ({
filterRowCount,
initialRowCount,
fileCount,
intl,
}) => {
if (filterRowCount === initialRowCount) {
return intl.formatMessage(
messages.rowStatusMessage,
{ fileCount, rowCount: initialRowCount },
);
}
return intl.formatMessage(
messages.rowStatusMessage,
{ fileCount, rowCount: filterRowCount },
);
};

View File

@@ -1,3 +1,21 @@
@import "files-and-videos/videos-page/transcript-settings/TranscriptSettings";
@import "files-and-videos/videos-page/VideoThumbnail";
@import "files-and-videos/generic/table-components/GalleryCard"
@import "files-and-videos/generic/table-components/GalleryCard";
.files-table {
#table-filters-dropdown {
visibility: hidden;
}
}
.pgn__form-control-set-inline {
.pgn__form-checkbox {
margin: 0;
}
width: 90%;
display: inline-grid;
grid-auto-flow: row;
gap: 24px 16px;
grid-template-columns: repeat(3, 33%);
}

View File

@@ -38,18 +38,7 @@ const VideoThumbnail = ({
}
const supportedFiles = videoImageSettings?.supportedFileFormats
? Object.values(videoImageSettings.supportedFileFormats) : null;
let isUploaded = false;
switch (status) {
case 'Ready':
isUploaded = true;
break;
case 'Imported':
isUploaded = true;
break;
default:
break;
}
const isUploaded = status === 'Success';
const showThumbnail = allowThumbnailUpload && thumbnail && isUploaded;

View File

@@ -1,5 +1,6 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { isEmpty } from 'lodash';
import { useDispatch, useSelector } from 'react-redux';
import {
injectIntl,
@@ -10,6 +11,7 @@ import {
useToggle,
ActionRow,
Button,
CheckboxFilter,
} from '@edx/paragon';
import Placeholder from '@edx/frontend-lib-content-components';
@@ -109,20 +111,33 @@ const VideosPage = ({
const transcriptColumn = {
id: 'transcripts',
Header: 'Transcript',
accessor: (({ transcripts }) => !isEmpty(transcripts)),
Cell: ({ row }) => {
const { transcripts } = row.original;
const numOfTranscripts = transcripts?.length;
return numOfTranscripts > 0 ? `(${numOfTranscripts}) available` : null;
},
Filter: CheckboxFilter,
filterChoices: [
{ name: intl.formatMessage(messages.transcribedCheckboxLabel), value: true },
{ name: intl.formatMessage(messages.notTranscribedCheckboxLabel), value: false },
],
};
const activeColumn = {
id: 'usageLocations',
Header: 'Active',
accessor: (({ usageLocations }) => !isEmpty(usageLocations)),
Cell: ({ row }) => ActiveColumn({ row }),
Filter: CheckboxFilter,
filterChoices: [
{ name: intl.formatMessage(messages.activeCheckboxLabel), value: true },
{ name: intl.formatMessage(messages.inactiveCheckboxLabel), value: false },
],
};
const durationColumn = {
id: 'duration',
Header: 'Video length',
accessor: 'duration',
Cell: ({ row }) => {
const { duration } = row.original;
return getFormattedDuration(duration);
@@ -132,6 +147,8 @@ const VideosPage = ({
id: 'status',
Header: '',
Cell: ({ row }) => StatusColumn({ row }),
Filter: CheckboxFilter,
filterChoices: [{ name: intl.formatMessage(messages.processingCheckboxLabel), value: 'Processing' }],
};
const videoThumbnailColumn = {
id: 'courseVideoImageUrl',

View File

@@ -336,38 +336,84 @@ describe('FilesAndUploads', () => {
expect(updateStatus).toEqual(RequestStatus.SUCCESSFUL);
});
it('sort button should be enabled and sort files by name', async () => {
renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
const sortsButton = screen.getByText(messages.sortButtonLabel.defaultMessage);
expect(sortsButton).toBeVisible();
describe('Sort and filter button', () => {
beforeEach(async () => {
renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
const sortAndFilterButton = screen.getByText(messages.sortButtonLabel.defaultMessage);
await waitFor(() => {
fireEvent.click(sortsButton);
expect(screen.getByText(messages.sortModalTitleLabel.defaultMessage)).toBeVisible();
await waitFor(() => {
fireEvent.click(sortAndFilterButton);
});
});
const sortNameAscendingButton = screen.getByText(messages.sortByNameAscending.defaultMessage);
fireEvent.click(sortNameAscendingButton);
fireEvent.click(screen.getByText(messages.applySortButton.defaultMessage));
expect(screen.queryByText(messages.sortModalTitleLabel.defaultMessage)).toBeNull();
});
describe('sort function', () => {
it('should be enabled and sort files by name', async () => {
const sortNameAscendingButton = screen.getByText(messages.sortByNameAscending.defaultMessage);
fireEvent.click(sortNameAscendingButton);
fireEvent.click(screen.getByText(messages.applySortButton.defaultMessage));
it('sort button should be enabled and sort files by file size', async () => {
renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
const sortsButton = screen.getByText(messages.sortButtonLabel.defaultMessage);
expect(sortsButton).toBeVisible();
expect(screen.queryByText(messages.sortModalTitleLabel.defaultMessage)).toBeNull();
});
await waitFor(() => {
fireEvent.click(sortsButton);
expect(screen.getByText(messages.sortModalTitleLabel.defaultMessage)).toBeVisible();
it('sort button should be enabled and sort files by file size', async () => {
const sortBySizeDescendingButton = screen.getByText(messages.sortBySizeDescending.defaultMessage);
fireEvent.click(sortBySizeDescendingButton);
fireEvent.click(screen.getByText(messages.applySortButton.defaultMessage));
expect(screen.queryByText(messages.sortModalTitleLabel.defaultMessage)).toBeNull();
});
});
const sortBySizeDescendingButton = screen.getByText(messages.sortBySizeDescending.defaultMessage);
fireEvent.click(sortBySizeDescendingButton);
fireEvent.click(screen.getByText(messages.applySortButton.defaultMessage));
expect(screen.queryByText(messages.sortModalTitleLabel.defaultMessage)).toBeNull();
describe('filter function', () => {
it('should filter videos with transcripts', async () => {
const notTranscribedCheckboxFilter = screen.getByText(
videoMessages.notTranscribedCheckboxLabel.defaultMessage,
);
const transcribedCheckboxFilter = screen.getByText(videoMessages.transcribedCheckboxLabel.defaultMessage);
fireEvent.click(transcribedCheckboxFilter);
fireEvent.click(notTranscribedCheckboxFilter);
fireEvent.click(transcribedCheckboxFilter);
fireEvent.click(screen.getByText(messages.applySortButton.defaultMessage));
const galleryCards = screen.getAllByTestId('grid-card', { exact: false });
expect(galleryCards).toHaveLength(1);
});
it('should clearAll selections', async () => {
const sortByNewest = screen.getByText(messages.sortByNewest.defaultMessage);
const sortBySizeDescendingButton = screen.getByText(messages.sortBySizeDescending.defaultMessage);
const transcribedCheckboxFilter = screen.getByLabelText(
videoMessages.transcribedCheckboxLabel.defaultMessage,
);
fireEvent.click(sortBySizeDescendingButton);
fireEvent.click(transcribedCheckboxFilter);
const clearAllButton = screen.getByText('Clear all');
await waitFor(() => fireEvent.click(clearAllButton));
expect(transcribedCheckboxFilter).toHaveProperty('checked', false);
expect(within(sortBySizeDescendingButton).getByLabelText('file size descending radio'))
.toHaveProperty('checked', false);
expect(within(sortByNewest).getByLabelText('date added descending radio'))
.toHaveProperty('checked', true);
});
it('should remove Transcribed filter chip', async () => {
const transcribedCheckboxFilter = screen.getByText(videoMessages.transcribedCheckboxLabel.defaultMessage);
fireEvent.click(transcribedCheckboxFilter);
fireEvent.click(screen.getByText(messages.applySortButton.defaultMessage));
const imageFilterChip = screen.getByTestId('icon-after');
fireEvent.click(imageFilterChip);
expect(screen.queryByText(videoMessages.transcribedCheckboxLabel.defaultMessage)).toBeNull();
});
});
});
});

View File

@@ -21,6 +21,7 @@ export const updateFileValues = (files) => {
clientVideoId,
created,
courseVideoImageUrl,
status,
} = file;
const wrapperType = 'video';
@@ -29,6 +30,13 @@ export const updateFileValues = (files) => {
thumbnail = `${getConfig().STUDIO_BASE_URL}${thumbnail}`;
}
let uploadStatus = status;
if (status === 'Ready' || status === 'Imported') {
uploadStatus = 'Success';
} else if (status === 'In Progress' || status === 'Uploaded') {
uploadStatus = 'Processing';
}
updatedFiles.push({
...file,
displayName: clientVideoId,
@@ -36,6 +44,7 @@ export const updateFileValues = (files) => {
wrapperType,
dateAdded: created.toString(),
usageLocations: [],
status: uploadStatus,
thumbnail,
});
});

View File

@@ -98,7 +98,7 @@ export const initialState = {
created: '',
courseVideoImageUrl: '/video',
transcripts: [],
status: 'Imported',
status: 'In Progress',
downloadLink: 'http://mOckID0.mp4',
},
},

View File

@@ -13,6 +13,26 @@ const messages = defineMessages({
id: 'course-authoring.video-uploads.thumbnail.alt',
defaultMessage: '{displayName} video thumbnail',
},
activeCheckboxLabel: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.activeCheckbox.label',
defaultMessage: 'Active',
},
inactiveCheckboxLabel: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.inactiveCheckbox.label',
defaultMessage: 'Inactive',
},
transcribedCheckboxLabel: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.transcribedCheckbox.label',
defaultMessage: 'Transcribed',
},
notTranscribedCheckboxLabel: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.notTranscribedCheckbox.label',
defaultMessage: 'Not transcribed',
},
processingCheckboxLabel: {
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.processingCheckbox.label',
defaultMessage: 'Processing',
},
});
export default messages;