diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicTable/BulkActionsBar/index.js b/packages/core/admin/admin/src/content-manager/components/DynamicTable/BulkActionsBar/index.js new file mode 100644 index 0000000000..b163a2e598 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/DynamicTable/BulkActionsBar/index.js @@ -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 && ( + <> + + + + )} + {showDelete && ( + <> + + + + )} + + ); +}; + +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; diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicTable/BulkActionsBar/tests/index.test.js b/packages/core/admin/admin/src/content-manager/components/DynamicTable/BulkActionsBar/tests/index.test.js new file mode 100644 index 0000000000..90202ed36b --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/DynamicTable/BulkActionsBar/tests/index.test.js @@ -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) => ( + + + + + + ); + + const setup = (props) => render(); + + 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([]); + }); +}); diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicTable/index.js b/packages/core/admin/admin/src/content-manager/components/DynamicTable/index.js index 721dbd8fb5..15f439be18 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicTable/index.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicTable/index.js @@ -9,13 +9,14 @@ import { INJECT_COLUMN_IN_TABLE } from '../../../exposedHooks'; import { selectDisplayedHeaders } from '../../pages/ListView/selectors'; import { getTrad } from '../../utils'; import TableRows from './TableRows'; -import ConfirmDialogDeleteAll from './ConfirmDialogDeleteAll'; import ConfirmDialogDelete from './ConfirmDialogDelete'; import { PublicationState } from './CellContent/PublicationState/PublicationState'; +import BulkActionsBar from './BulkActionsBar'; const DynamicTable = ({ canCreate, canDelete, + canPublish, contentTypeName, action, isBulkable, @@ -89,17 +90,25 @@ const DynamicTable = ({ return ( ( + + )} > { - const [entriesToDelete, setEntriesToDelete] = useState([]); + const [selectedEntries, setSelectedEntries] = useState([]); const [showConfirmDeleteAll, setShowConfirmDeleteAll] = useState(false); const [showConfirmDelete, setShowConfirmDelete] = useState(false); const [isConfirmButtonLoading, setIsConfirmButtonLoading] = useState(false); @@ -44,7 +45,7 @@ const Table = ({ const ROW_COUNT = rows.length + 1; const COL_COUNT = headers.length + (withBulkActions ? 1 : 0) + (withMainAction ? 1 : 0); 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 ? { @@ -57,9 +58,9 @@ const Table = ({ const handleConfirmDeleteAll = async () => { try { setIsConfirmButtonLoading(true); - await onConfirmDeleteAll(entriesToDelete); + await onConfirmDeleteAll(selectedEntries); handleToggleConfirmDeleteAll(); - setEntriesToDelete([]); + setSelectedEntries([]); setIsConfirmButtonLoading(false); } catch (err) { setIsConfirmButtonLoading(false); @@ -71,7 +72,7 @@ const Table = ({ try { setIsConfirmButtonLoading(true); // await onConfirmDeleteAll(entriesToDelete); - await onConfirmDelete(entriesToDelete[0]); + await onConfirmDelete(selectedEntries[0]); handleToggleConfirmDelete(); setIsConfirmButtonLoading(false); } catch (err) { @@ -82,9 +83,9 @@ const Table = ({ const handleSelectAll = () => { if (!areAllEntriesSelected) { - setEntriesToDelete(rows.map((row) => row.id)); + setSelectedEntries(rows.map((row) => row.id)); } else { - setEntriesToDelete([]); + setSelectedEntries([]); } }; @@ -98,19 +99,19 @@ const Table = ({ const handleToggleConfirmDelete = () => { if (showConfirmDelete) { - setEntriesToDelete([]); + setSelectedEntries([]); } setShowConfirmDelete((prev) => !prev); }; const handleClickDelete = (id) => { - setEntriesToDelete([id]); + setSelectedEntries([id]); handleToggleConfirmDelete(); }; const handleSelectRow = ({ name, value }) => { - setEntriesToDelete((prev) => { + setSelectedEntries((prev) => { if (value) { return prev.concat(name); } @@ -119,6 +120,10 @@ const Table = ({ }); }; + const clearSelectedEntries = () => { + setSelectedEntries([]); + }; + const ConfirmDeleteAllComponent = components?.ConfirmDialogDeleteAll ? components.ConfirmDialogDeleteAll : ConfirmDialog; @@ -129,7 +134,7 @@ const Table = ({ return ( <> - {entriesToDelete.length > 0 && ( + {selectedEntries.length > 0 && ( @@ -140,17 +145,21 @@ const Table = ({ id: 'content-manager.components.TableDelete.label', defaultMessage: '{number, plural, one {# entry} other {# entries}} selected', }, - { number: entriesToDelete.length } + { number: selectedEntries.length } )} - + {renderBulkActionsBar ? ( + renderBulkActionsBar({ selectedEntries, clearSelectedEntries }) + ) : ( + + )} @@ -159,7 +168,7 @@ const Table = ({ cloneElement(child, { - entriesToDelete, + entriesToDelete: selectedEntries, onClickDelete: handleClickDelete, onSelectRow: handleSelectRow, headers, @@ -219,6 +228,7 @@ Table.defaultProps = { rows: [], withBulkActions: false, withMainAction: false, + renderBulkActionsBar: undefined, }; Table.propTypes = { @@ -248,6 +258,7 @@ Table.propTypes = { rows: PropTypes.array, withBulkActions: PropTypes.bool, withMainAction: PropTypes.bool, + renderBulkActionsBar: PropTypes.func, }; export default Table;