feat: add sort function and modal (#577)
* feat: add sort modal and function * fix: dateAdded typo * chore: update mock api data
This commit is contained in:
@@ -17,7 +17,6 @@ import {
|
||||
CheckboxControl,
|
||||
} from '@edx/paragon';
|
||||
import { ContentCopy, InfoOutline } from '@edx/paragon/icons';
|
||||
import { getUtcDateTime } from './data/utils';
|
||||
import AssetThumbnail from './FileThumbnail';
|
||||
|
||||
import messages from './messages';
|
||||
@@ -36,8 +35,6 @@ const FileInfo = ({
|
||||
setLockedState(locked);
|
||||
handleLockedAsset(asset.id, locked);
|
||||
};
|
||||
const dateAdded = getUtcDateTime(asset.dateAdded);
|
||||
|
||||
return (
|
||||
<ModalDialog
|
||||
title={asset.displayName}
|
||||
@@ -71,7 +68,7 @@ const FileInfo = ({
|
||||
<FormattedMessage {...messages.dateAddedTitle} />
|
||||
</div>
|
||||
<FormattedDate
|
||||
value={dateAdded}
|
||||
value={asset.dateAdded}
|
||||
year="numeric"
|
||||
month="short"
|
||||
day="2-digit"
|
||||
|
||||
@@ -23,7 +23,9 @@ import {
|
||||
deleteAssetFile,
|
||||
fetchAssets,
|
||||
updateAssetLock,
|
||||
updateAssetOrder,
|
||||
} from './data/thunks';
|
||||
import { sortFiles } from './data/utils';
|
||||
import messages from './messages';
|
||||
|
||||
import FileInput, { fileInput } from './FileInput';
|
||||
@@ -72,7 +74,6 @@ const FilesAndUploads = ({
|
||||
setAddOpen,
|
||||
});
|
||||
const assets = useModels('assets', assetIds);
|
||||
|
||||
const handleDropzoneAsset = ({ fileData, handleError }) => {
|
||||
try {
|
||||
const file = fileData.get('file');
|
||||
@@ -82,6 +83,11 @@ const FilesAndUploads = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSort = (sortType) => {
|
||||
const newAssetIdOrder = sortFiles(assets, sortType);
|
||||
dispatch(updateAssetOrder(courseId, newAssetIdOrder, sortType));
|
||||
};
|
||||
|
||||
const handleBulkDelete = () => {
|
||||
closeDeleteConfirmation();
|
||||
setDeleteOpen();
|
||||
@@ -117,6 +123,7 @@ const FilesAndUploads = ({
|
||||
{...{
|
||||
selectedFlatRows,
|
||||
fileInputControl,
|
||||
handleSort,
|
||||
handleBulkDownload,
|
||||
handleOpenDeleteConfirmation,
|
||||
}}
|
||||
@@ -229,6 +236,22 @@ const FilesAndUploads = ({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
Header: 'Locked',
|
||||
accessor: 'locked',
|
||||
// Filter: CheckboxFilter,
|
||||
// filter: 'exactText',
|
||||
// filterChoices: [
|
||||
// {
|
||||
// name: 'Locked',
|
||||
// value: true,
|
||||
// },
|
||||
// {
|
||||
// name: 'Unlocked',
|
||||
// value: false,
|
||||
// },
|
||||
// ],
|
||||
},
|
||||
]}
|
||||
itemCount={totalCount}
|
||||
pageCount={Math.ceil(totalCount / 50)}
|
||||
|
||||
@@ -205,6 +205,34 @@ describe('FilesAndUploads', () => {
|
||||
expect(deleteStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||
expect(screen.queryByTestId('grid-card-mOckID1')).toBeNull();
|
||||
});
|
||||
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();
|
||||
await waitFor(() => {
|
||||
fireEvent.click(sortsButton);
|
||||
expect(screen.getByText(messages.sortModalTitleLabel.defaultMessage)).toBeVisible();
|
||||
});
|
||||
const sortNameAscendingButton = screen.getByText(messages.sortByNameAscending.defaultMessage);
|
||||
fireEvent.click(sortNameAscendingButton);
|
||||
fireEvent.click(screen.getByText(messages.applySortButton.defaultMessage));
|
||||
expect(screen.queryByText(messages.sortModalTitleLabel.defaultMessage)).toBeNull();
|
||||
});
|
||||
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();
|
||||
await waitFor(() => {
|
||||
fireEvent.click(sortsButton);
|
||||
expect(screen.getByText(messages.sortModalTitleLabel.defaultMessage)).toBeVisible();
|
||||
});
|
||||
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('card menu actions', () => {
|
||||
it('should open asset info', async () => {
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
updateErrors,
|
||||
} from './slice';
|
||||
|
||||
import { getWrapperType } from './utils';
|
||||
import { updateFileValues } from './utils';
|
||||
|
||||
export function fetchAssets(courseId) {
|
||||
return async (dispatch) => {
|
||||
@@ -32,8 +32,8 @@ export function fetchAssets(courseId) {
|
||||
try {
|
||||
const { totalCount } = await getAssets(courseId);
|
||||
const { assets } = await getAssets(courseId, totalCount);
|
||||
const assetsWithWraperType = getWrapperType(assets);
|
||||
dispatch(addModels({ modelType: 'assets', models: assetsWithWraperType }));
|
||||
const parsedAssests = updateFileValues(assets);
|
||||
dispatch(addModels({ modelType: 'assets', models: parsedAssests }));
|
||||
dispatch(setAssetIds({
|
||||
assetIds: assets.map(asset => asset.id),
|
||||
}));
|
||||
@@ -49,6 +49,14 @@ export function fetchAssets(courseId) {
|
||||
};
|
||||
}
|
||||
|
||||
export function updateAssetOrder(courseId, assetIds) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.IN_PROGRESS }));
|
||||
dispatch(setAssetIds({ assetIds }));
|
||||
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.SUCCESSFUL }));
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteAssetFile(courseId, id, totalCount) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateDeletingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
@@ -72,10 +80,10 @@ export function addAssetFile(courseId, file, totalCount) {
|
||||
|
||||
try {
|
||||
const { asset } = await addAsset(courseId, file);
|
||||
const [assetsWithWraperType] = getWrapperType([asset]);
|
||||
const [parsedAssest] = updateFileValues([asset]);
|
||||
dispatch(addModel({
|
||||
modelType: 'assets',
|
||||
model: { ...assetsWithWraperType },
|
||||
model: { ...parsedAssest },
|
||||
}));
|
||||
dispatch(addAssetSuccess({
|
||||
assetId: asset.id,
|
||||
|
||||
@@ -6,22 +6,28 @@ ensureConfig([
|
||||
'STUDIO_BASE_URL',
|
||||
], 'Course Apps API service');
|
||||
|
||||
export const getWrapperType = (assets) => {
|
||||
const assetsWithWraperType = [];
|
||||
assets.forEach(asset => {
|
||||
if (FILES_AND_UPLOAD_TYPE_FILTERS.images.includes(asset.contentType)) {
|
||||
assetsWithWraperType.push({ wrapperType: 'image', ...asset });
|
||||
} else if (FILES_AND_UPLOAD_TYPE_FILTERS.documents.includes(asset.contentType)) {
|
||||
assetsWithWraperType.push({ wrapperType: 'document', ...asset });
|
||||
} else if (FILES_AND_UPLOAD_TYPE_FILTERS.code.includes(asset.contentType)) {
|
||||
assetsWithWraperType.push({ wrapperType: 'code', ...asset });
|
||||
} else if (FILES_AND_UPLOAD_TYPE_FILTERS.audio.includes(asset.contentType)) {
|
||||
assetsWithWraperType.push({ wrapperType: 'audio', ...asset });
|
||||
} else {
|
||||
assetsWithWraperType.push({ wrapperType: 'other', ...asset });
|
||||
export const updateFileValues = (files) => {
|
||||
const updatedFiles = [];
|
||||
files.forEach(file => {
|
||||
let wrapperType = 'other';
|
||||
if (FILES_AND_UPLOAD_TYPE_FILTERS.images.includes(file.contentType)) {
|
||||
wrapperType = 'image';
|
||||
} else if (FILES_AND_UPLOAD_TYPE_FILTERS.documents.includes(file.contentType)) {
|
||||
wrapperType = 'document';
|
||||
} else if (FILES_AND_UPLOAD_TYPE_FILTERS.code.includes(file.contentType)) {
|
||||
wrapperType = 'code';
|
||||
} else if (FILES_AND_UPLOAD_TYPE_FILTERS.audio.includes(file.contentType)) {
|
||||
wrapperType = 'audio';
|
||||
}
|
||||
|
||||
const { dateAdded } = file;
|
||||
const utcDateString = dateAdded.replace(/\bat\b/g, '');
|
||||
const utcDateTime = new Date(utcDateString).toString();
|
||||
|
||||
updatedFiles.push({ ...file, wrapperType, dateAdded: utcDateTime });
|
||||
});
|
||||
return assetsWithWraperType;
|
||||
|
||||
return updatedFiles;
|
||||
};
|
||||
|
||||
export const getSrc = ({ thumbnail, wrapperType, externalUrl }) => {
|
||||
@@ -40,8 +46,35 @@ export const getSrc = ({ thumbnail, wrapperType, externalUrl }) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getUtcDateTime = (date) => {
|
||||
const utcDateString = date.replace(/\bat\b/g, '');
|
||||
const utcDateTime = new Date(utcDateString);
|
||||
return utcDateTime;
|
||||
export const sortFiles = (files, sortType) => {
|
||||
const [sort, direction] = sortType.split(',');
|
||||
let sortedFiles;
|
||||
if (sort === 'displayName') {
|
||||
sortedFiles = files.sort((f1, f2) => {
|
||||
const lowerCaseF1 = f1[sort].toLowerCase();
|
||||
const lowerCaseF2 = f2[sort].toLowerCase();
|
||||
if (lowerCaseF1 < lowerCaseF2) {
|
||||
return 1;
|
||||
}
|
||||
if (lowerCaseF1 > lowerCaseF2) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
} else {
|
||||
sortedFiles = files.sort((f1, f2) => {
|
||||
if (f1[sort] < f2[sort]) {
|
||||
return 1;
|
||||
}
|
||||
if (f1[sort] > f2[sort]) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
const sortedIds = sortedFiles.map(file => file.id);
|
||||
if (direction === 'asc') {
|
||||
return sortedIds.reverse();
|
||||
}
|
||||
return sortedIds;
|
||||
};
|
||||
|
||||
@@ -48,6 +48,16 @@ export const generateFetchAssetApiResponse = () => ({
|
||||
dateAdded: '',
|
||||
thumbnail: '/asset',
|
||||
},
|
||||
{
|
||||
id: 'mOckID5',
|
||||
displayName: 'mOckID5',
|
||||
locked: false,
|
||||
externalUrl: 'static_tab_1',
|
||||
portableUrl: '',
|
||||
contentType: 'application/json',
|
||||
dateAdded: 'Aug 13, 2023 at 22:08 UTC',
|
||||
thumbnail: null,
|
||||
},
|
||||
{
|
||||
id: 'mOckID3',
|
||||
displayName: 'mOckID3',
|
||||
@@ -55,7 +65,7 @@ export const generateFetchAssetApiResponse = () => ({
|
||||
externalUrl: 'static_tab_1',
|
||||
portableUrl: '',
|
||||
contentType: 'application/pdf',
|
||||
dateAdded: '',
|
||||
dateAdded: 'Aug 17, 2023 at 22:08 UTC',
|
||||
thumbnail: null,
|
||||
},
|
||||
{
|
||||
@@ -68,16 +78,6 @@ export const generateFetchAssetApiResponse = () => ({
|
||||
dateAdded: '',
|
||||
thumbnail: null,
|
||||
},
|
||||
{
|
||||
id: 'mOckID5',
|
||||
displayName: 'mOckID5',
|
||||
locked: false,
|
||||
externalUrl: 'static_tab_1',
|
||||
portableUrl: '',
|
||||
contentType: 'application/json',
|
||||
dateAdded: '',
|
||||
thumbnail: null,
|
||||
},
|
||||
{
|
||||
id: 'mOckID6',
|
||||
displayName: 'mOckID6',
|
||||
@@ -88,6 +88,16 @@ export const generateFetchAssetApiResponse = () => ({
|
||||
dateAdded: '',
|
||||
thumbnail: null,
|
||||
},
|
||||
{
|
||||
id: 'mOckID6-2',
|
||||
displayName: 'mOckID6',
|
||||
locked: false,
|
||||
externalUrl: 'static_tab_1',
|
||||
portableUrl: 'May 17, 2023 at 22:08 UTC',
|
||||
contentType: 'application/octet-stream',
|
||||
dateAdded: '',
|
||||
thumbnail: null,
|
||||
},
|
||||
],
|
||||
totalCount: 50,
|
||||
});
|
||||
|
||||
@@ -114,9 +114,45 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Delete',
|
||||
},
|
||||
cancelButtonLabel: {
|
||||
id: 'course-authoring.files-and-uploads.deleteConfirmation.cancelButton.label',
|
||||
id: 'course-authoring.files-and-uploads.cancelButton.label',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
sortButtonLabel: {
|
||||
id: 'course-authoring.files-and-uploads.sortButton.label',
|
||||
defaultMessage: 'Sort',
|
||||
},
|
||||
sortModalTitleLabel: {
|
||||
id: 'course-authoring.files-and-uploads.sortModal.title',
|
||||
defaultMessage: 'Sort by',
|
||||
},
|
||||
sortByNameAscending: {
|
||||
id: 'course-authoring.files-and-uploads.sortByNameAscendingButton.label',
|
||||
defaultMessage: 'Name (A-Z)',
|
||||
},
|
||||
sortByNewest: {
|
||||
id: 'course-authoring.files-and-uploads.sortByNewestButton.label',
|
||||
defaultMessage: 'Newest',
|
||||
},
|
||||
sortBySizeDescending: {
|
||||
id: 'course-authoring.files-and-uploads.sortBySizeDescendingButton.label',
|
||||
defaultMessage: 'File size (High to low)',
|
||||
},
|
||||
sortByNameDescending: {
|
||||
id: 'course-authoring.files-and-uploads.sortByNameDescendingButton.label',
|
||||
defaultMessage: 'Name (Z-A)',
|
||||
},
|
||||
sortByOldest: {
|
||||
id: 'course-authoring.files-and-uploads.sortByOldestButton.label',
|
||||
defaultMessage: 'Oldest',
|
||||
},
|
||||
sortBySizeAscending: {
|
||||
id: 'course-authoring.files-and-uploads.sortBySizeAscendingButton.label',
|
||||
defaultMessage: 'File size(Low to high)',
|
||||
},
|
||||
applySortButton: {
|
||||
id: 'course-authoring.files-and-uploads.applyySortButton.label',
|
||||
defaultMessage: 'Apply',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,48 +1,156 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { PropTypes } from 'prop-types';
|
||||
import { injectIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Dropdown } from '@edx/paragon';
|
||||
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
Button,
|
||||
Dropdown,
|
||||
ModalDialog,
|
||||
SelectableBox,
|
||||
useToggle,
|
||||
} from '@edx/paragon';
|
||||
import { Add } from '@edx/paragon/icons';
|
||||
import messages from '../messages';
|
||||
|
||||
const TableActions = ({
|
||||
selectedFlatRows,
|
||||
fileInputControl,
|
||||
handleSort,
|
||||
handleBulkDownload,
|
||||
handleOpenDeleteConfirmation,
|
||||
}) => (
|
||||
<>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="actions-menu-toggle"
|
||||
alt="actions-menu-toggle"
|
||||
variant="outline-primary"
|
||||
// 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}>
|
||||
<FormattedMessage {...messages.sortButtonLabel} />
|
||||
</Button>
|
||||
<Dropdown className="mx-2">
|
||||
<Dropdown.Toggle
|
||||
id="actions-menu-toggle"
|
||||
alt="actions-menu-toggle"
|
||||
variant="outline-primary"
|
||||
>
|
||||
<FormattedMessage {...messages.actionsButtonLabel} />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
onClick={() => handleBulkDownload(selectedFlatRows)}
|
||||
disabled={_.isEmpty(selectedFlatRows)}
|
||||
>
|
||||
<FormattedMessage {...messages.downloadTitle} />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
data-testid="open-delete-confirmation-button"
|
||||
onClick={() => handleOpenDeleteConfirmation(selectedFlatRows)}
|
||||
disabled={_.isEmpty(selectedFlatRows)}
|
||||
>
|
||||
<FormattedMessage {...messages.deleteTitle} />
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<Button iconBefore={Add} onClick={fileInputControl.click}>
|
||||
<FormattedMessage {...messages.addFilesButtonLabel} />
|
||||
</Button>
|
||||
<ModalDialog
|
||||
title={intl.formatMessage(messages.sortModalTitleLabel)}
|
||||
isOpen={isSortOpen}
|
||||
onClose={closeSort}
|
||||
size="lg"
|
||||
hasCloseButton
|
||||
>
|
||||
<FormattedMessage {...messages.actionsButtonLabel} />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
onClick={() => handleBulkDownload(selectedFlatRows)}
|
||||
disabled={_.isEmpty(selectedFlatRows)}
|
||||
>
|
||||
<FormattedMessage {...messages.downloadTitle} />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
data-testid="open-delete-confirmation-button"
|
||||
onClick={() => handleOpenDeleteConfirmation(selectedFlatRows)}
|
||||
disabled={_.isEmpty(selectedFlatRows)}
|
||||
>
|
||||
<FormattedMessage {...messages.deleteTitle} />
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<Button iconBefore={Add} onClick={fileInputControl.click} className="ml-2">
|
||||
<FormattedMessage {...messages.addFilesButtonLabel} />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
TableActions.defaultProps = {
|
||||
selectedFlatRows: null,
|
||||
@@ -66,6 +174,9 @@ TableActions.propTypes = {
|
||||
}).isRequired,
|
||||
handleOpenDeleteConfirmation: PropTypes.func.isRequired,
|
||||
handleBulkDownload: PropTypes.func.isRequired,
|
||||
handleSort: PropTypes.func.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(TableActions);
|
||||
|
||||
Reference in New Issue
Block a user