Skip to content

Commit

Permalink
Hide suppression unless library administrator (PP-1463) (#121)
Browse files Browse the repository at this point in the history
* Restrict per-library book suppression to a library's manager.

* Get tests working again.

* New tests.
  • Loading branch information
tdilauro authored Jul 23, 2024
1 parent 499e39d commit 430422a
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 6 deletions.
6 changes: 6 additions & 0 deletions src/components/BookDetailsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import * as React from "react";
import { Store } from "@reduxjs/toolkit";
import * as PropTypes from "prop-types";

import Admin from "../models/Admin";
import BookDetailsTabContainer from "./BookDetailsTabContainer";
import BookDetails from "./BookDetails";
import { BookDetailsContainerProps } from "@thepalaceproject/web-opds-client/lib/components/Root";
import { RootState } from "../store";

export interface BookDetailsContainerContext {
admin: Admin;
csrfToken: string;
tab: string;
editorStore: Store<RootState>;
Expand All @@ -34,13 +36,17 @@ const BookDetailsContainer = (
library={context.library}
csrfToken={context.csrfToken}
store={context.editorStore}
canSuppress={context.admin.isLibraryManager(
context.library(props.collectionUrl, props.bookUrl)
)}
>
{book}
</BookDetailsTabContainer>
</div>
);
};
BookDetailsContainer.contextTypes = {
admin: PropTypes.object.isRequired,
csrfToken: PropTypes.string.isRequired,
tab: PropTypes.string,
editorStore: PropTypes.object.isRequired,
Expand Down
21 changes: 15 additions & 6 deletions src/components/BookDetailsEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface BookDetailsEditorOwnProps {
csrfToken: string;
store?: Store<RootState>;
refreshCatalog?: () => Promise<any>;
canSuppress: boolean;
}

const connector = connect(mapStateToProps, mapDispatchToProps);
Expand Down Expand Up @@ -54,6 +55,14 @@ export class BookDetailsEditor extends React.Component<BookDetailsEditorProps> {

render(): React.ReactElement {
const { bookData } = this.props;
const canSuppress =
!!bookData?.suppressPerLibraryLink && this.props.canSuppress;
const canUnsuppress =
!!bookData?.unsuppressPerLibraryLink && this.props.canSuppress;
// A button row will be present if:
// - There is a refresh link; or
// - There is a un/suppress link and the admin is allowed to use it.
const hasButtonRow = bookData?.refreshLink || canSuppress || canUnsuppress;
return (
<div className="book-details-editor">
{bookData && !this.props.fetchError && (
Expand All @@ -66,11 +75,9 @@ export class BookDetailsEditor extends React.Component<BookDetailsEditorProps> {
<ErrorMessage error={this.props.editError} />
)}

{(bookData.suppressPerLibraryLink ||
bookData.unsuppressPerLibraryLink ||
bookData.refreshLink) && (
{hasButtonRow && (
<div className="form-group form-inline">
{!!bookData.suppressPerLibraryLink && (
{canSuppress && (
<BookDetailsEditorSuppression
link={bookData.suppressPerLibraryLink}
onConfirm={() =>
Expand Down Expand Up @@ -100,7 +107,7 @@ export class BookDetailsEditor extends React.Component<BookDetailsEditorProps> {
}
/>
)}
{!!bookData.unsuppressPerLibraryLink && (
{canUnsuppress && (
<BookDetailsEditorSuppression
link={bookData.unsuppressPerLibraryLink}
onConfirm={() =>
Expand All @@ -109,7 +116,9 @@ export class BookDetailsEditor extends React.Component<BookDetailsEditorProps> {
)
}
onComplete={this.refresh}
buttonDisabled={this.props.isFetching}
buttonDisabled={
this.props.isFetching || !this.props.canSuppress
}
buttonContent="Restore"
buttonTitle="Restore availability for this library."
className="left-align"
Expand Down
3 changes: 3 additions & 0 deletions src/components/BookDetailsTabContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import BookCoverEditor from "./BookCoverEditor";
import CustomListsForBook from "./CustomListsForBook";
import { TabContainer, TabContainerProps } from "./TabContainer";
import { RootState } from "../store";
import Admin from "../models/Admin";

interface BookDetailsTabContainerOwnProps extends TabContainerProps {
bookUrl: string;
collectionUrl: string;
refreshCatalog: () => Promise<any>;
library: (collectionUrl, bookUrl) => string;
canSuppress: boolean;
// extended from TabContainerProps in superclass
// store?: Store<RootState>;
// csrfToken?: string;
Expand Down Expand Up @@ -61,6 +63,7 @@ export class BookDetailsTabContainer extends TabContainer<
csrfToken={this.props.csrfToken}
bookUrl={this.props.bookUrl}
refreshCatalog={this.props.refreshCatalog}
canSuppress={this.props.canSuppress}
/>
);
tabs["classifications"] = (
Expand Down
1 change: 1 addition & 0 deletions src/components/__tests__/BookDetailsContainer-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe("BookDetailsContainer", () => {
editorStore: store,
csrfToken: "token",
library: stub(),
admin: { isLibraryManager: () => true },
};
refreshCatalog = stub();

Expand Down
9 changes: 9 additions & 0 deletions src/components/__tests__/BookDetailsEditor-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe("BookDetailsEditor", () => {
bookUrl={permalink}
{...dispatchProps}
csrfToken={"token"}
canSuppress={true}
/>
);

Expand All @@ -68,6 +69,7 @@ describe("BookDetailsEditor", () => {
bookUrl={permalink}
{...dispatchProps}
csrfToken={"token"}
canSuppress={true}
/>
);
wrapper.setProps({ bookUrl: newPermalink });
Expand All @@ -83,6 +85,7 @@ describe("BookDetailsEditor", () => {
bookData={{ id: "id", title: "title" }}
bookUrl="url"
csrfToken="token"
canSuppress={true}
{...dispatchProps}
/>
);
Expand All @@ -101,6 +104,7 @@ describe("BookDetailsEditor", () => {
bookData={{ id: "id", title: "title", suppressPerLibraryLink }}
bookUrl="url"
csrfToken="token"
canSuppress={true}
{...dispatchProps}
/>
);
Expand All @@ -118,6 +122,7 @@ describe("BookDetailsEditor", () => {
bookData={{ id: "id", title: "title", unsuppressPerLibraryLink }}
bookUrl="url"
csrfToken="token"
canSuppress={true}
{...dispatchProps}
/>
);
Expand All @@ -137,6 +142,7 @@ describe("BookDetailsEditor", () => {
bookData={{ id: "id", title: "title", refreshLink: refreshLink }}
bookUrl="url"
csrfToken="token"
canSuppress={true}
{...dispatchProps}
/>
);
Expand All @@ -158,6 +164,7 @@ describe("BookDetailsEditor", () => {
bookData={{ id: "id", title: "title" }}
bookUrl="url"
csrfToken="token"
canSuppress={true}
fetchError={fetchError}
{...dispatchProps}
/>
Expand All @@ -184,6 +191,7 @@ describe("BookDetailsEditor", () => {
bookData={{ id: "id", title: "title", editLink: editLink }}
bookUrl="url"
csrfToken="token"
canSuppress={true}
editError={editError}
{...dispatchProps}
/>
Expand Down Expand Up @@ -217,6 +225,7 @@ describe("BookDetailsEditor", () => {
bookData={{ id: "id", title: "title", editLink }}
bookUrl="url"
csrfToken="token"
canSuppress={true}
{...dispatchProps}
roles={roles}
media={media}
Expand Down
1 change: 1 addition & 0 deletions src/components/__tests__/BookDetailsTabContainer-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe("BookDetailsTabContainer", () => {
refreshCatalog={stub()}
store={store}
library={(a, b) => "library"}
canSuppress={true}
// from store
complaintsCount={0}
bookData={null}
Expand Down
36 changes: 36 additions & 0 deletions tests/jest/components/BookEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,40 @@ describe("BookDetails", () => {
fetchMock.restore();
});

it("don't show hide button if not a library's admin", () => {
const { queryByRole } = renderWithProviders(
<BookDetailsEditor
bookData={{ id: "id", title: "title", suppressPerLibraryLink }}
bookUrl="url"
csrfToken="token"
canSuppress={false}
{...dispatchProps}
/>
);
const hideButton = queryByRole("button", { name: "Hide" });
const restoreButton = queryByRole("button", { name: "Restore" });

expect(hideButton).to.be.null;
expect(restoreButton).to.be.null;
});

it("don't show restore button if not a library's admin", () => {
const { queryByRole } = renderWithProviders(
<BookDetailsEditor
bookData={{ id: "id", title: "title", unsuppressPerLibraryLink }}
bookUrl="url"
csrfToken="token"
canSuppress={false}
{...dispatchProps}
/>
);
const hideButton = queryByRole("button", { name: "Hide" });
const restoreButton = queryByRole("button", { name: "Restore" });

expect(hideButton).to.be.null;
expect(restoreButton).to.be.null;
});

it("uses modal for suppress book confirmation", async () => {
// Configure standard constructors so that RTK Query works in tests with FetchMockJest
Object.assign(fetchMock.config, {
Expand All @@ -96,6 +130,7 @@ describe("BookDetails", () => {
bookData={{ id: "id", title: "title", suppressPerLibraryLink }}
bookUrl="url"
csrfToken="token"
canSuppress={true}
{...dispatchProps}
/>
);
Expand Down Expand Up @@ -154,6 +189,7 @@ describe("BookDetails", () => {
bookData={{ id: "id", title: "title", unsuppressPerLibraryLink }}
bookUrl="url"
csrfToken="token"
canSuppress={true}
{...dispatchProps}
/>
);
Expand Down

0 comments on commit 430422a

Please sign in to comment.