From 430422aeb2e23dfc8601408ba31c6cab690ae701 Mon Sep 17 00:00:00 2001 From: Tim DiLauro Date: Tue, 23 Jul 2024 19:12:33 -0400 Subject: [PATCH] Hide suppression unless library administrator (PP-1463) (#121) * Restrict per-library book suppression to a library's manager. * Get tests working again. * New tests. --- src/components/BookDetailsContainer.tsx | 6 ++++ src/components/BookDetailsEditor.tsx | 21 +++++++---- src/components/BookDetailsTabContainer.tsx | 3 ++ .../__tests__/BookDetailsContainer-test.tsx | 1 + .../__tests__/BookDetailsEditor-test.tsx | 9 +++++ .../BookDetailsTabContainer-test.tsx | 1 + tests/jest/components/BookEditor.test.tsx | 36 +++++++++++++++++++ 7 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/components/BookDetailsContainer.tsx b/src/components/BookDetailsContainer.tsx index 243962525..008d4b0b5 100644 --- a/src/components/BookDetailsContainer.tsx +++ b/src/components/BookDetailsContainer.tsx @@ -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; @@ -34,6 +36,9 @@ const BookDetailsContainer = ( library={context.library} csrfToken={context.csrfToken} store={context.editorStore} + canSuppress={context.admin.isLibraryManager( + context.library(props.collectionUrl, props.bookUrl) + )} > {book} @@ -41,6 +46,7 @@ const BookDetailsContainer = ( ); }; BookDetailsContainer.contextTypes = { + admin: PropTypes.object.isRequired, csrfToken: PropTypes.string.isRequired, tab: PropTypes.string, editorStore: PropTypes.object.isRequired, diff --git a/src/components/BookDetailsEditor.tsx b/src/components/BookDetailsEditor.tsx index 750e10c79..bc9e12c41 100644 --- a/src/components/BookDetailsEditor.tsx +++ b/src/components/BookDetailsEditor.tsx @@ -18,6 +18,7 @@ export interface BookDetailsEditorOwnProps { csrfToken: string; store?: Store; refreshCatalog?: () => Promise; + canSuppress: boolean; } const connector = connect(mapStateToProps, mapDispatchToProps); @@ -54,6 +55,14 @@ export class BookDetailsEditor extends React.Component { 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 (
{bookData && !this.props.fetchError && ( @@ -66,11 +75,9 @@ export class BookDetailsEditor extends React.Component { )} - {(bookData.suppressPerLibraryLink || - bookData.unsuppressPerLibraryLink || - bookData.refreshLink) && ( + {hasButtonRow && (
- {!!bookData.suppressPerLibraryLink && ( + {canSuppress && ( @@ -100,7 +107,7 @@ export class BookDetailsEditor extends React.Component { } /> )} - {!!bookData.unsuppressPerLibraryLink && ( + {canUnsuppress && ( @@ -109,7 +116,9 @@ export class BookDetailsEditor extends React.Component { ) } 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" diff --git a/src/components/BookDetailsTabContainer.tsx b/src/components/BookDetailsTabContainer.tsx index 1c5cdd9c9..578791828 100644 --- a/src/components/BookDetailsTabContainer.tsx +++ b/src/components/BookDetailsTabContainer.tsx @@ -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; library: (collectionUrl, bookUrl) => string; + canSuppress: boolean; // extended from TabContainerProps in superclass // store?: Store; // csrfToken?: string; @@ -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"] = ( diff --git a/src/components/__tests__/BookDetailsContainer-test.tsx b/src/components/__tests__/BookDetailsContainer-test.tsx index f0a59caa7..bd8a16668 100644 --- a/src/components/__tests__/BookDetailsContainer-test.tsx +++ b/src/components/__tests__/BookDetailsContainer-test.tsx @@ -32,6 +32,7 @@ describe("BookDetailsContainer", () => { editorStore: store, csrfToken: "token", library: stub(), + admin: { isLibraryManager: () => true }, }; refreshCatalog = stub(); diff --git a/src/components/__tests__/BookDetailsEditor-test.tsx b/src/components/__tests__/BookDetailsEditor-test.tsx index 31e4fa212..4ad89c89b 100644 --- a/src/components/__tests__/BookDetailsEditor-test.tsx +++ b/src/components/__tests__/BookDetailsEditor-test.tsx @@ -49,6 +49,7 @@ describe("BookDetailsEditor", () => { bookUrl={permalink} {...dispatchProps} csrfToken={"token"} + canSuppress={true} /> ); @@ -68,6 +69,7 @@ describe("BookDetailsEditor", () => { bookUrl={permalink} {...dispatchProps} csrfToken={"token"} + canSuppress={true} /> ); wrapper.setProps({ bookUrl: newPermalink }); @@ -83,6 +85,7 @@ describe("BookDetailsEditor", () => { bookData={{ id: "id", title: "title" }} bookUrl="url" csrfToken="token" + canSuppress={true} {...dispatchProps} /> ); @@ -101,6 +104,7 @@ describe("BookDetailsEditor", () => { bookData={{ id: "id", title: "title", suppressPerLibraryLink }} bookUrl="url" csrfToken="token" + canSuppress={true} {...dispatchProps} /> ); @@ -118,6 +122,7 @@ describe("BookDetailsEditor", () => { bookData={{ id: "id", title: "title", unsuppressPerLibraryLink }} bookUrl="url" csrfToken="token" + canSuppress={true} {...dispatchProps} /> ); @@ -137,6 +142,7 @@ describe("BookDetailsEditor", () => { bookData={{ id: "id", title: "title", refreshLink: refreshLink }} bookUrl="url" csrfToken="token" + canSuppress={true} {...dispatchProps} /> ); @@ -158,6 +164,7 @@ describe("BookDetailsEditor", () => { bookData={{ id: "id", title: "title" }} bookUrl="url" csrfToken="token" + canSuppress={true} fetchError={fetchError} {...dispatchProps} /> @@ -184,6 +191,7 @@ describe("BookDetailsEditor", () => { bookData={{ id: "id", title: "title", editLink: editLink }} bookUrl="url" csrfToken="token" + canSuppress={true} editError={editError} {...dispatchProps} /> @@ -217,6 +225,7 @@ describe("BookDetailsEditor", () => { bookData={{ id: "id", title: "title", editLink }} bookUrl="url" csrfToken="token" + canSuppress={true} {...dispatchProps} roles={roles} media={media} diff --git a/src/components/__tests__/BookDetailsTabContainer-test.tsx b/src/components/__tests__/BookDetailsTabContainer-test.tsx index 63d418360..43e531e76 100644 --- a/src/components/__tests__/BookDetailsTabContainer-test.tsx +++ b/src/components/__tests__/BookDetailsTabContainer-test.tsx @@ -33,6 +33,7 @@ describe("BookDetailsTabContainer", () => { refreshCatalog={stub()} store={store} library={(a, b) => "library"} + canSuppress={true} // from store complaintsCount={0} bookData={null} diff --git a/tests/jest/components/BookEditor.test.tsx b/tests/jest/components/BookEditor.test.tsx index 84f5ed2df..cd15364d9 100644 --- a/tests/jest/components/BookEditor.test.tsx +++ b/tests/jest/components/BookEditor.test.tsx @@ -80,6 +80,40 @@ describe("BookDetails", () => { fetchMock.restore(); }); + it("don't show hide button if not a library's admin", () => { + const { queryByRole } = renderWithProviders( + + ); + 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( + + ); + 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, { @@ -96,6 +130,7 @@ describe("BookDetails", () => { bookData={{ id: "id", title: "title", suppressPerLibraryLink }} bookUrl="url" csrfToken="token" + canSuppress={true} {...dispatchProps} /> ); @@ -154,6 +189,7 @@ describe("BookDetails", () => { bookData={{ id: "id", title: "title", unsuppressPerLibraryLink }} bookUrl="url" csrfToken="token" + canSuppress={true} {...dispatchProps} /> );