Skip to content

Commit

Permalink
Add support for batch editing selected items on the Search screen
Browse files Browse the repository at this point in the history
  • Loading branch information
adamjarling committed Dec 8, 2020
1 parent a069467 commit eaa06fe
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 104 deletions.
129 changes: 103 additions & 26 deletions assets/js/components/Search/ActionRow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,72 @@ import { Button } from "@nulib/admin-react-components";
import SearchBatchModal from "@js/components/Search/BatchModal";

export default function SearchActionRow({
handleCsvExport,
handleCsvExportAllItems,
handleCsvExportItems,
handleDeselectAll,
handleEditAllItems,
handleEditItems,
handleViewAndEdit,
numberOfResults,
selectedItems = [],
}) {
const [isModalOpen, setIsModalOpen] = React.useState(false);
const [isModalAllItemsOpen, setIsModalAllItemsOpen] = React.useState(false);
const [isModalItemsOpen, setIsModalItemsOpen] = React.useState(false);
const numSelectedItems = selectedItems.length;

function handleCsvExportAllItemsClick() {
setIsModalAllItemsOpen(false);
handleCsvExportAllItems();
}

function handleCsvExportItemsClick() {
setIsModalItemsOpen(false);
handleCsvExportItems();
}

function handleEditAllItemsClick() {
setIsModalAllItemsOpen(false);
handleEditAllItems();
}

function handleEditItemsClick() {
setIsModalItemsOpen(false);
handleEditItems();
}

function handleViewAndEditClick() {
setIsModalItemsOpen(false);
handleViewAndEdit();
}

return (
<React.Fragment>
<div className="field is-grouped" data-testid="search-action-row">
<p className="control">
<Button
isLight
onClick={() => setIsModalOpen(!isModalOpen)}
onClick={() => setIsModalAllItemsOpen(!isModalAllItemsOpen)}
disabled={selectedItems.length > 0}
data-testid="select-all-button"
data-testid="button-select-all"
>
<span>Select All</span>
</Button>
</p>
<p className="control">
<Button
isLight
data-testid="view-and-edit-button"
data-testid="button-edit-items"
disabled={selectedItems.length === 0}
onClick={handleViewAndEdit}
onClick={() => setIsModalItemsOpen(!isModalItemsOpen)}
>
<span className="icon">
<FontAwesomeIcon icon="eye" />
</span>
<span>View and edit {selectedItems.length} Items</span>
<span>Edit {selectedItems.length} Items</span>
</Button>
</p>
{selectedItems.length > 0 && (
<p className="control">
<Button
isLight
data-testid="deselect-all-button"
data-testid="button-deselect-all"
onClick={handleDeselectAll}
>
<span className="icon">
Expand All @@ -54,30 +80,81 @@ export default function SearchActionRow({
</Button>
</p>
)}
<p className="control">
<Button isLight disabled>
<span className="icon">
<FontAwesomeIcon icon="file-csv" />
</span>
<span>Export CSV</span>
</Button>
</p>
</div>

{/* Batch edit ALL items */}
<SearchBatchModal
handleCloseClick={() => setIsModalAllItemsOpen(false)}
isOpen={isModalAllItemsOpen}
>
<Button
className="is-fullwidth mb-4"
data-testid="button-batch-all-edit"
onClick={handleEditAllItemsClick}
>
<span className="icon">
<FontAwesomeIcon icon="edit" />
</span>
<span>Batch edit {numberOfResults} works</span>
</Button>
<Button
className="is-fullwidth"
data-testid="button-csv-all-export"
onClick={handleCsvExportAllItemsClick}
>
<span className="icon">
<FontAwesomeIcon icon="file-csv" />
</span>
<span>Export metadata from {numberOfResults} works </span>
</Button>
</SearchBatchModal>

{/* Batch edit selected items */}
<SearchBatchModal
handleCloseClick={() => setIsModalOpen(false)}
handleCsvExport={handleCsvExport}
handleEditAllItems={handleEditAllItems}
isOpen={isModalOpen}
numberOfResults={numberOfResults}
/>
handleCloseClick={() => setIsModalItemsOpen(false)}
isOpen={isModalItemsOpen}
>
<Button
className="is-fullwidth mb-4"
data-testid="button-batch-items-edit"
onClick={handleEditItemsClick}
>
<span className="icon">
<FontAwesomeIcon icon="edit" />
</span>
<span>Batch edit {numSelectedItems} works</span>
</Button>
<Button
className="is-fullwidth mb-4"
data-testid="button-view-and-edit"
onClick={handleViewAndEditClick}
>
<span className="icon">
<FontAwesomeIcon icon="eye" />
</span>
<span>View and edit {numSelectedItems} individual works</span>
</Button>
<Button
className="is-fullwidth"
data-testid="button-csv-items-export"
onClick={handleCsvExportItemsClick}
>
<span className="icon">
<FontAwesomeIcon icon="file-csv" />
</span>
<span>Export metadata from {numSelectedItems} works </span>
</Button>
</SearchBatchModal>
</React.Fragment>
);
}

SearchActionRow.propTypes = {
handleCsvExport: PropTypes.func.isRequired,
handleCsvExportAllItems: PropTypes.func.isRequired,
handleCsvExportItems: PropTypes.func.isRequired,
handleDeselectAll: PropTypes.func,
handleEditAllItems: PropTypes.func.isRequired,
handleEditItems: PropTypes.func.isRequired,
handleViewAndEdit: PropTypes.func.isRequired,
numberOfResults: PropTypes.number,
selectedItems: PropTypes.array,
Expand Down
58 changes: 40 additions & 18 deletions assets/js/components/Search/ActionRow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import SearchActionRow from "./ActionRow";
import userEvent from "@testing-library/user-event";

let props = {
handleCsvExport: jest.fn(),
handleCsvExportAllItems: jest.fn(),
handleCsvExportItems: jest.fn(),
handleDeselectAll: jest.fn(),
handleEditAllItems: jest.fn(),
handleEditItems: jest.fn(),
handleViewAndEdit: jest.fn(),
numberOfResults: 33,
selectedItems: [],
Expand All @@ -18,29 +20,49 @@ describe("SearchActionRow component", () => {
expect(screen.getByTestId("search-action-row"));
});

it("renders 'select all' button enabled by default, and 'edit X items' button disabled by default ", () => {
render(<SearchActionRow {...props} selectedItems={[]} />);
describe("with no selected items", () => {
beforeEach(() => {
render(<SearchActionRow {...props} selectedItems={[]} />);
});

const selectAllButton = screen.getByTestId("select-all-button");
it("renders the select all and edit buttons", () => {
expect(screen.getByTestId("button-select-all")).not.toBeDisabled();
expect(screen.getByTestId("button-edit-items")).toBeDisabled();
});

expect(selectAllButton).not.toBeDisabled();
expect(screen.getByTestId("view-and-edit-button")).toBeDisabled();
it("renders 'batch edit' and 'csv export' buttons when select all button is clicked", () => {
userEvent.click(screen.getByTestId("button-select-all"));
userEvent.click(screen.getByTestId("button-batch-all-edit"));
expect(props.handleEditAllItems).toHaveBeenCalled();
userEvent.click(screen.getByTestId("button-csv-all-export"));
expect(props.handleCsvExportAllItems).toHaveBeenCalled();
});
});

it("renders a disabled 'select all' button, and enabled 'view and edit' and 'deselect all' buttons when search items are selected", () => {
render(<SearchActionRow {...props} selectedItems={["abc", "dfg"]} />);
describe("with selected items", () => {
beforeEach(() => {
render(<SearchActionRow {...props} selectedItems={["abc", "def"]} />);
});

const viewAndEditButton = screen.getByTestId("view-and-edit-button");
const deselectAll = screen.getByTestId("deselect-all-button");
it("renders the disabled select all and enabled edit and deselect all buttons", () => {
expect(screen.getByTestId("button-select-all")).toBeDisabled();
expect(screen.getByTestId("button-edit-items")).not.toBeDisabled();
expect(screen.getByTestId("button-deselect-all")).not.toBeDisabled();
});

expect(screen.getByTestId("select-all-button")).toBeDisabled();
expect(viewAndEditButton).not.toBeDisabled();
expect(deselectAll).not.toBeDisabled();
it("calls the deselect all callback function when button clicked", () => {
userEvent.click(screen.getByTestId("button-deselect-all"));
expect(props.handleDeselectAll).toHaveBeenCalled();
});

userEvent.click(viewAndEditButton);
expect(props.handleViewAndEdit).toHaveBeenCalled();

userEvent.click(deselectAll);
expect(props.handleDeselectAll).toHaveBeenCalled();
it("renders 'batch edit' and 'csv export' buttons when select all button is clicked", () => {
userEvent.click(screen.getByTestId("button-edit-items"));
userEvent.click(screen.getByTestId("button-batch-items-edit"));
expect(props.handleEditItems).toHaveBeenCalled();
userEvent.click(screen.getByTestId("button-view-and-edit"));
expect(props.handleViewAndEdit).toHaveBeenCalled();
userEvent.click(screen.getByTestId("button-csv-items-export"));
expect(props.handleCsvExportItems).toHaveBeenCalled();
});
});
});
43 changes: 4 additions & 39 deletions assets/js/components/Search/BatchModal.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import React from "react";
import PropTypes from "prop-types";
import { Button } from "@nulib/admin-react-components";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

function SearchBatchModal({
handleCloseClick,
handleCsvExport,
handleEditAllItems,
isOpen,
numberOfResults,
}) {
function SearchBatchModal({ children, handleCloseClick, isOpen }) {
return (
<div
className={`modal ${isOpen ? "is-active" : ""}`}
Expand All @@ -23,36 +16,10 @@ function SearchBatchModal({
className="delete"
aria-label="close"
onClick={handleCloseClick}
data-testid="header-close-button"
></button>
</header>
<section className="modal-card-body">
<div className="columns">
<div className="column">
<Button
className="is-fullwidth"
data-testid="button-batch-edit"
onClick={handleEditAllItems}
>
<span className="icon">
<FontAwesomeIcon icon="edit" />
</span>
<span>Batch edit {numberOfResults} works</span>
</Button>
</div>
<div className="column">
<Button
className="is-fullwidth"
data-testid="button-csv-export"
onClick={handleCsvExport}
>
<span className="icon">
<FontAwesomeIcon icon="file-csv" />
</span>
<span>Export metadata from {numberOfResults} works </span>
</Button>
</div>
</div>
</section>
<section className="modal-card-body">{children}</section>
<footer className="modal-card-foot is-justify-content-flex-end">
<Button isText onClick={handleCloseClick}>
Cancel
Expand All @@ -64,11 +31,9 @@ function SearchBatchModal({
}

SearchBatchModal.propTypes = {
children: PropTypes.node,
handleCloseClick: PropTypes.func.isRequired,
handleCsvExport: PropTypes.func.isRequired,
handleEditAllItems: PropTypes.func.isRequired,
isOpen: PropTypes.bool,
numberOfResults: PropTypes.number.isRequired,
};

export default SearchBatchModal;
26 changes: 16 additions & 10 deletions assets/js/components/Search/BatchModal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,37 @@ import userEvent from "@testing-library/user-event";

let props = {
handleCloseClick: jest.fn(),
handleCsvExport: jest.fn(),
handleEditAllItems: jest.fn(),
isOpen: false,
numberOfResults: 35,
children: (
<div>
<p>Im child content</p>
</div>
),
};

describe("SearchBatchModal component", () => {
it("renders hidden state", () => {
it("renders modal hidden state", () => {
render(<SearchBatchModal {...props} />);
expect(screen.getByTestId("select-all-modal")).not.toHaveClass("is-active");
});

it("renders visible state", () => {
it("renders modal visible state", () => {
props = { ...props, isOpen: true };
render(<SearchBatchModal {...props} />);
expect(screen.getByTestId("select-all-modal")).toHaveClass("is-active");
});

it("calls the appropriate callback functions on button clicks", () => {
it("calls the close modal callback function", () => {
props = { ...props, isOpen: true };
render(<SearchBatchModal {...props} />);
userEvent.click(screen.getByTestId("button-batch-edit"));
userEvent.click(screen.getByTestId("button-csv-export"));
userEvent.click(screen.getByTestId("header-close-button"));
userEvent.click(screen.getByText("Cancel"));
expect(props.handleCloseClick).toHaveBeenCalledTimes(2);
});

expect(props.handleCsvExport).toHaveBeenCalled();
expect(props.handleEditAllItems).toHaveBeenCalled();
it("renders child content", () => {
props = { ...props, isOpen: true };
render(<SearchBatchModal {...props} />);
expect(screen.getByText("Im child content"));
});
});
Loading

0 comments on commit eaa06fe

Please sign in to comment.