Merge pull request #16515 from strapi/bulk-publish/actions-bar

[Bulk Publish] Add new publish buttons to the content manager tables
This commit is contained in:
Fernando Chávez 2023-04-27 14:41:51 +02:00 committed by GitHub
commit b83c8d9320
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 222 additions and 26 deletions

View File

@ -0,0 +1,85 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Button } from '@strapi/design-system';
import { useIntl } from 'react-intl';
import { useTracking } from '@strapi/helper-plugin';
import ConfirmDialogDeleteAll from '../ConfirmDialogDeleteAll';
const BulkActionsBar = ({
showPublish,
showDelete,
onConfirmDeleteAll,
selectedEntries,
clearSelectedEntries,
}) => {
const { formatMessage } = useIntl();
const { trackUsage } = useTracking();
const [isConfirmButtonLoading, setIsConfirmButtonLoading] = useState(false);
const [showConfirmDeleteAll, setShowConfirmDeleteAll] = useState(false);
const handleToggleShowDeleteAllModal = () => {
if (!showConfirmDeleteAll) {
trackUsage('willBulkDeleteEntries');
}
setShowConfirmDeleteAll((prev) => !prev);
};
const handleConfirmDeleteAll = async () => {
try {
setIsConfirmButtonLoading(true);
await onConfirmDeleteAll(selectedEntries);
handleToggleShowDeleteAllModal();
clearSelectedEntries();
setIsConfirmButtonLoading(false);
} catch (err) {
setIsConfirmButtonLoading(false);
handleToggleShowDeleteAllModal();
}
};
return (
<>
{showPublish && (
<>
<Button variant="tertiary">
{formatMessage({ id: 'app.utils.publish', defaultMessage: 'Publish' })}
</Button>
<Button variant="tertiary">
{formatMessage({ id: 'app.utils.unpublish', defaultMessage: 'Unpublish' })}
</Button>
</>
)}
{showDelete && (
<>
<Button variant="danger-light" onClick={handleToggleShowDeleteAllModal}>
{formatMessage({ id: 'global.delete', defaultMessage: 'Delete' })}
</Button>
<ConfirmDialogDeleteAll
isOpen={showConfirmDeleteAll}
onToggleDialog={handleToggleShowDeleteAllModal}
isConfirmButtonLoading={isConfirmButtonLoading}
onConfirm={handleConfirmDeleteAll}
/>
</>
)}
</>
);
};
BulkActionsBar.defaultProps = {
showPublish: false,
showDelete: false,
onConfirmDeleteAll() {},
};
BulkActionsBar.propTypes = {
showPublish: PropTypes.bool,
showDelete: PropTypes.bool,
onConfirmDeleteAll: PropTypes.func,
selectedEntries: PropTypes.array.isRequired,
clearSelectedEntries: PropTypes.func.isRequired,
};
export default BulkActionsBar;

View File

@ -0,0 +1,87 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { ThemeProvider, lightTheme } from '@strapi/design-system';
import { IntlProvider } from 'react-intl';
import BulkActionsBar from '../index';
import { act } from 'react-dom/test-utils';
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
useTracking: () => ({
trackUsage: jest.fn(),
}),
}));
jest.mock('../../../../../shared/hooks', () => ({
...jest.requireActual('../../../../../shared/hooks'),
useInjectionZone: () => [],
}));
describe('BulkActionsBar', () => {
const requiredProps = {
selectedEntries: [],
clearSelectedEntries: jest.fn(),
};
const TestComponent = (props) => (
<ThemeProvider theme={lightTheme}>
<IntlProvider locale="en" messages={{}} defaultLocale="en">
<BulkActionsBar {...requiredProps} {...props} />
</IntlProvider>
</ThemeProvider>
);
const setup = (props) => render(<TestComponent {...props} />);
it('should render publish buttons if showPublish is true', () => {
setup({ showPublish: true });
expect(screen.getByRole('button', { name: /\bPublish\b/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /\bUnpublish\b/ })).toBeInTheDocument();
});
it('should not render publish buttons if showPublish is false', () => {
setup({ showPublish: false });
expect(screen.queryByRole('button', { name: /\bPublish\b/ })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /\bUnpublish\b/ })).not.toBeInTheDocument();
});
it('should render delete button if showDelete is true', () => {
setup({ showDelete: true });
expect(screen.getByRole('button', { name: /\bDelete\b/ })).toBeInTheDocument();
});
it('should not render delete button if showDelete is false', () => {
setup({ showDelete: false });
expect(screen.queryByRole('button', { name: /\bDelete\b/ })).not.toBeInTheDocument();
});
it('should show delete modal if delete button is clicked', () => {
setup({ showDelete: true });
act(() => {
fireEvent.click(screen.getByRole('button', { name: /\bDelete\b/ }));
});
expect(screen.getByText('Confirmation')).toBeInTheDocument();
});
it('should call confirm delete all if confirmation button is clicked', async () => {
const mockConfirmDeleteAll = jest.fn();
setup({
showDelete: true,
onConfirmDeleteAll: mockConfirmDeleteAll,
});
await act(async () => {
await fireEvent.click(screen.getByRole('button', { name: /\bDelete\b/ }));
fireEvent.click(screen.getByRole('button', { name: /confirm/i }));
});
expect(mockConfirmDeleteAll).toHaveBeenCalledWith([]);
});
});

View File

@ -9,13 +9,14 @@ import { INJECT_COLUMN_IN_TABLE } from '../../../exposedHooks';
import { selectDisplayedHeaders } from '../../pages/ListView/selectors'; import { selectDisplayedHeaders } from '../../pages/ListView/selectors';
import { getTrad } from '../../utils'; import { getTrad } from '../../utils';
import TableRows from './TableRows'; import TableRows from './TableRows';
import ConfirmDialogDeleteAll from './ConfirmDialogDeleteAll';
import ConfirmDialogDelete from './ConfirmDialogDelete'; import ConfirmDialogDelete from './ConfirmDialogDelete';
import { PublicationState } from './CellContent/PublicationState/PublicationState'; import { PublicationState } from './CellContent/PublicationState/PublicationState';
import BulkActionsBar from './BulkActionsBar';
const DynamicTable = ({ const DynamicTable = ({
canCreate, canCreate,
canDelete, canDelete,
canPublish,
contentTypeName, contentTypeName,
action, action,
isBulkable, isBulkable,
@ -89,17 +90,25 @@ const DynamicTable = ({
return ( return (
<Table <Table
components={{ ConfirmDialogDelete, ConfirmDialogDeleteAll }} components={{ ConfirmDialogDelete }}
contentType={contentTypeName} contentType={contentTypeName}
action={action} action={action}
isLoading={isLoading} isLoading={isLoading}
headers={tableHeaders} headers={tableHeaders}
onConfirmDelete={onConfirmDelete} onConfirmDelete={onConfirmDelete}
onConfirmDeleteAll={onConfirmDeleteAll}
onOpenDeleteAllModalTrackedEvent="willBulkDeleteEntries" onOpenDeleteAllModalTrackedEvent="willBulkDeleteEntries"
rows={rows} rows={rows}
withBulkActions withBulkActions
withMainAction={canDelete && isBulkable} withMainAction={(canDelete || canPublish) && isBulkable}
renderBulkActionsBar={({ selectedEntries, clearSelectedEntries }) => (
<BulkActionsBar
showPublish={canPublish && hasDraftAndPublish}
showDelete={canDelete}
onConfirmDeleteAll={onConfirmDeleteAll}
selectedEntries={selectedEntries}
clearSelectedEntries={clearSelectedEntries}
/>
)}
> >
<TableRows <TableRows
canCreate={canCreate} canCreate={canCreate}
@ -121,6 +130,7 @@ DynamicTable.defaultProps = {
DynamicTable.propTypes = { DynamicTable.propTypes = {
canCreate: PropTypes.bool.isRequired, canCreate: PropTypes.bool.isRequired,
canDelete: PropTypes.bool.isRequired, canDelete: PropTypes.bool.isRequired,
canPublish: PropTypes.bool.isRequired,
contentTypeName: PropTypes.string.isRequired, contentTypeName: PropTypes.string.isRequired,
action: PropTypes.node, action: PropTypes.node,
isBulkable: PropTypes.bool.isRequired, isBulkable: PropTypes.bool.isRequired,

View File

@ -64,6 +64,7 @@ function ListView({
canCreate, canCreate,
canDelete, canDelete,
canRead, canRead,
canPublish,
data, data,
getData, getData,
getDataSucceeded, getDataSucceeded,
@ -331,6 +332,7 @@ function ListView({
<DynamicTable <DynamicTable
canCreate={canCreate} canCreate={canCreate}
canDelete={canDelete} canDelete={canDelete}
canPublish={canPublish}
contentTypeName={headerLayoutTitle} contentTypeName={headerLayoutTitle}
onConfirmDeleteAll={handleConfirmDeleteAllData} onConfirmDeleteAll={handleConfirmDeleteAllData}
onConfirmDelete={handleConfirmDeleteData} onConfirmDelete={handleConfirmDeleteData}
@ -355,6 +357,7 @@ ListView.propTypes = {
canCreate: PropTypes.bool.isRequired, canCreate: PropTypes.bool.isRequired,
canDelete: PropTypes.bool.isRequired, canDelete: PropTypes.bool.isRequired,
canRead: PropTypes.bool.isRequired, canRead: PropTypes.bool.isRequired,
canPublish: PropTypes.bool.isRequired,
data: PropTypes.array.isRequired, data: PropTypes.array.isRequired,
layout: PropTypes.exact({ layout: PropTypes.exact({
components: PropTypes.object.isRequired, components: PropTypes.object.isRequired,

View File

@ -32,9 +32,10 @@ const Table = ({
rows, rows,
withBulkActions, withBulkActions,
withMainAction, withMainAction,
renderBulkActionsBar,
...rest ...rest
}) => { }) => {
const [entriesToDelete, setEntriesToDelete] = useState([]); const [selectedEntries, setSelectedEntries] = useState([]);
const [showConfirmDeleteAll, setShowConfirmDeleteAll] = useState(false); const [showConfirmDeleteAll, setShowConfirmDeleteAll] = useState(false);
const [showConfirmDelete, setShowConfirmDelete] = useState(false); const [showConfirmDelete, setShowConfirmDelete] = useState(false);
const [isConfirmButtonLoading, setIsConfirmButtonLoading] = useState(false); const [isConfirmButtonLoading, setIsConfirmButtonLoading] = useState(false);
@ -44,7 +45,7 @@ const Table = ({
const ROW_COUNT = rows.length + 1; const ROW_COUNT = rows.length + 1;
const COL_COUNT = headers.length + (withBulkActions ? 1 : 0) + (withMainAction ? 1 : 0); const COL_COUNT = headers.length + (withBulkActions ? 1 : 0) + (withMainAction ? 1 : 0);
const hasFilters = query?.filters !== undefined; const hasFilters = query?.filters !== undefined;
const areAllEntriesSelected = entriesToDelete.length === rows.length && rows.length > 0; const areAllEntriesSelected = selectedEntries.length === rows.length && rows.length > 0;
const content = hasFilters const content = hasFilters
? { ? {
@ -57,9 +58,9 @@ const Table = ({
const handleConfirmDeleteAll = async () => { const handleConfirmDeleteAll = async () => {
try { try {
setIsConfirmButtonLoading(true); setIsConfirmButtonLoading(true);
await onConfirmDeleteAll(entriesToDelete); await onConfirmDeleteAll(selectedEntries);
handleToggleConfirmDeleteAll(); handleToggleConfirmDeleteAll();
setEntriesToDelete([]); setSelectedEntries([]);
setIsConfirmButtonLoading(false); setIsConfirmButtonLoading(false);
} catch (err) { } catch (err) {
setIsConfirmButtonLoading(false); setIsConfirmButtonLoading(false);
@ -71,7 +72,7 @@ const Table = ({
try { try {
setIsConfirmButtonLoading(true); setIsConfirmButtonLoading(true);
// await onConfirmDeleteAll(entriesToDelete); // await onConfirmDeleteAll(entriesToDelete);
await onConfirmDelete(entriesToDelete[0]); await onConfirmDelete(selectedEntries[0]);
handleToggleConfirmDelete(); handleToggleConfirmDelete();
setIsConfirmButtonLoading(false); setIsConfirmButtonLoading(false);
} catch (err) { } catch (err) {
@ -82,9 +83,9 @@ const Table = ({
const handleSelectAll = () => { const handleSelectAll = () => {
if (!areAllEntriesSelected) { if (!areAllEntriesSelected) {
setEntriesToDelete(rows.map((row) => row.id)); setSelectedEntries(rows.map((row) => row.id));
} else { } else {
setEntriesToDelete([]); setSelectedEntries([]);
} }
}; };
@ -98,19 +99,19 @@ const Table = ({
const handleToggleConfirmDelete = () => { const handleToggleConfirmDelete = () => {
if (showConfirmDelete) { if (showConfirmDelete) {
setEntriesToDelete([]); setSelectedEntries([]);
} }
setShowConfirmDelete((prev) => !prev); setShowConfirmDelete((prev) => !prev);
}; };
const handleClickDelete = (id) => { const handleClickDelete = (id) => {
setEntriesToDelete([id]); setSelectedEntries([id]);
handleToggleConfirmDelete(); handleToggleConfirmDelete();
}; };
const handleSelectRow = ({ name, value }) => { const handleSelectRow = ({ name, value }) => {
setEntriesToDelete((prev) => { setSelectedEntries((prev) => {
if (value) { if (value) {
return prev.concat(name); return prev.concat(name);
} }
@ -119,6 +120,10 @@ const Table = ({
}); });
}; };
const clearSelectedEntries = () => {
setSelectedEntries([]);
};
const ConfirmDeleteAllComponent = components?.ConfirmDialogDeleteAll const ConfirmDeleteAllComponent = components?.ConfirmDialogDeleteAll
? components.ConfirmDialogDeleteAll ? components.ConfirmDialogDeleteAll
: ConfirmDialog; : ConfirmDialog;
@ -129,7 +134,7 @@ const Table = ({
return ( return (
<> <>
{entriesToDelete.length > 0 && ( {selectedEntries.length > 0 && (
<Box> <Box>
<Box paddingBottom={4}> <Box paddingBottom={4}>
<Flex justifyContent="space-between"> <Flex justifyContent="space-between">
@ -140,17 +145,21 @@ const Table = ({
id: 'content-manager.components.TableDelete.label', id: 'content-manager.components.TableDelete.label',
defaultMessage: '{number, plural, one {# entry} other {# entries}} selected', defaultMessage: '{number, plural, one {# entry} other {# entries}} selected',
}, },
{ number: entriesToDelete.length } { number: selectedEntries.length }
)} )}
</Typography> </Typography>
<Button {renderBulkActionsBar ? (
onClick={handleToggleConfirmDeleteAll} renderBulkActionsBar({ selectedEntries, clearSelectedEntries })
startIcon={<Trash />} ) : (
size="L" <Button
variant="danger-light" onClick={handleToggleConfirmDeleteAll}
> startIcon={<Trash />}
{formatMessage({ id: 'global.delete', defaultMessage: 'Delete' })} size="L"
</Button> variant="danger-light"
>
{formatMessage({ id: 'global.delete', defaultMessage: 'Delete' })}
</Button>
)}
</BlockActions> </BlockActions>
</Flex> </Flex>
</Box> </Box>
@ -159,7 +168,7 @@ const Table = ({
<TableCompo colCount={COL_COUNT} rowCount={ROW_COUNT} footer={footer}> <TableCompo colCount={COL_COUNT} rowCount={ROW_COUNT} footer={footer}>
<TableHead <TableHead
areAllEntriesSelected={areAllEntriesSelected} areAllEntriesSelected={areAllEntriesSelected}
entriesToDelete={entriesToDelete} entriesToDelete={selectedEntries}
headers={headers} headers={headers}
onSelectAll={handleSelectAll} onSelectAll={handleSelectAll}
withMainAction={withMainAction} withMainAction={withMainAction}
@ -175,7 +184,7 @@ const Table = ({
) : ( ) : (
Children.toArray(children).map((child) => Children.toArray(children).map((child) =>
cloneElement(child, { cloneElement(child, {
entriesToDelete, entriesToDelete: selectedEntries,
onClickDelete: handleClickDelete, onClickDelete: handleClickDelete,
onSelectRow: handleSelectRow, onSelectRow: handleSelectRow,
headers, headers,
@ -219,6 +228,7 @@ Table.defaultProps = {
rows: [], rows: [],
withBulkActions: false, withBulkActions: false,
withMainAction: false, withMainAction: false,
renderBulkActionsBar: undefined,
}; };
Table.propTypes = { Table.propTypes = {
@ -248,6 +258,7 @@ Table.propTypes = {
rows: PropTypes.array, rows: PropTypes.array,
withBulkActions: PropTypes.bool, withBulkActions: PropTypes.bool,
withMainAction: PropTypes.bool, withMainAction: PropTypes.bool,
renderBulkActionsBar: PropTypes.func,
}; };
export default Table; export default Table;