From 7f3010ce1e41f3e4f8594bf66599acd786264dc6 Mon Sep 17 00:00:00 2001 From: John Hatton <hattonjohn@gmail.com> Date: Mon, 6 Apr 2020 18:17:41 -0600 Subject: [PATCH] Use Publisher field for Publisher Group instead of bookshelves Also, allow grid filtering on publisher & original publisher --- src/IFilter.ts | 4 +- src/components/BookShelfGroup.tsx | 44 ++++++--- src/components/BulkEdit/AddTagPanel.tsx | 8 +- .../BulkEdit/AssignPublisherPanel.tsx | 8 +- src/components/BulkEdit/BulkEditPanel.tsx | 14 +-- src/components/Grid/GridColumns.tsx | 91 +++++++++++-------- src/components/Grid/GridControlInternal.tsx | 2 +- src/components/HomePage.tsx | 8 +- src/components/PublisherGroup.tsx | 51 +++++++++++ src/components/PublisherPages.tsx | 28 ++++++ src/connection/LibraryQueryHooks.ts | 8 ++ 11 files changed, 191 insertions(+), 75 deletions(-) create mode 100644 src/components/PublisherGroup.tsx diff --git a/src/IFilter.ts b/src/IFilter.ts index 5e940fce..fd79be4a 100644 --- a/src/IFilter.ts +++ b/src/IFilter.ts @@ -1,12 +1,14 @@ export enum InCirculationOptions { All, No, - Yes + Yes, } export interface IFilter { language?: string; // review: what is this exactly? BCP 47? Our Parse has duplicate "ethnologueCode" and "isoCode" columns, which actually contain code and full script tags. // publisher?: string; bookshelf?: string; + publisher?: string; + originalPublisher?: string; feature?: string; topic?: string; bookShelfCategory?: string; diff --git a/src/components/BookShelfGroup.tsx b/src/components/BookShelfGroup.tsx index 6945ade6..bb18ea53 100644 --- a/src/components/BookShelfGroup.tsx +++ b/src/components/BookShelfGroup.tsx @@ -28,14 +28,11 @@ interface IProps { // Normally the bookshelf name matches the image name, but if not we change it here: const nameToImageMap = new Map<string, string>([ - // // something in our pipeline won't deliver an image that starts with "3" - ["3Asafeer", "Asafeer"], - ["Room To Read", "Room to Read"], ["Ministerio de EducaciĆ³n de Guatemala", "Guatemala MOE"], - ["Resources for the Blind, Inc. (Philippines)", "Resources for the Blind"] + ["Resources for the Blind, Inc. (Philippines)", "Resources for the Blind"], ]); -export const BookshelfGroup: React.FunctionComponent<IProps> = props => { +export const BookshelfGroup: React.FunctionComponent<IProps> = (props) => { // At this point there are so few bookshelves that we just retrieve the whole list and then filter here. // Might be a good thing to cache. const bookshelfResults = useGetBookshelvesByCategory( @@ -49,26 +46,26 @@ export const BookshelfGroup: React.FunctionComponent<IProps> = props => { // From that we need to determine that on this level, we should be showing [painting, sculpture]. const bookshelfPathsAtThisLevel = props.pathToTheCurrentLevel - ? bookshelfResults.filter(b => + ? bookshelfResults.filter((b) => b.key.startsWith(props.pathToTheCurrentLevel!) ) : bookshelfResults; const prefix: string = props.pathToTheCurrentLevel || ""; const allNamesAtThisLevel = bookshelfPathsAtThisLevel - .map(b => b.key.replace(prefix, "")) - .map(name => { + .map((b) => b.key.replace(prefix, "")) + .map((name) => { const i = name.indexOf("/"); return i < 0 ? name : name.substr(0, i); }); const uniqueNamesAtThisLevel = [ - ...Array.from(new Set(allNamesAtThisLevel)) + ...Array.from(new Set(allNamesAtThisLevel)), ]; const { bookshelves } = useContext(CachedTablesContext); - const cards = + const bookshelfCards = bookshelfResults && uniqueNamesAtThisLevel.sort().map((nextLevel: string) => { const imageName = nameToImageMap.get(nextLevel) ?? nextLevel; @@ -86,7 +83,7 @@ export const BookshelfGroup: React.FunctionComponent<IProps> = props => { title={bookshelf.displayName || ""} bookCount="??" filter={{ - bookshelf: fullBookshelfKey + bookshelf: fullBookshelfKey, }} pageType={props.bookShelfCategory} img={ @@ -98,5 +95,30 @@ export const BookshelfGroup: React.FunctionComponent<IProps> = props => { ); }); + // enhance: once we get the Publisher field filled in, we can switch to getting a full list of publishers + // and then us the following group + const publishers = ["Little Zebra Books"]; // temporary until we switch over instead of hard-coding "Little Zebra" + const cardsFromPublisherField = + bookshelfResults && + publishers.sort().map((publisher) => { + return ( + <CategoryCard + key={publisher} + //preTitle={publisher} + title={publisher} + bookCount="??" + filter={{ + publisher, + }} + pageType={props.bookShelfCategory} + img={ + "https://share.bloomlibrary.org/bookshelf-images/" + + encodeUrl(publisher) + + ".png" + } + /> + ); + }); + const cards = bookshelfCards.concat(cardsFromPublisherField); return <CategoryCardGroup {...props}>{cards}</CategoryCardGroup>; }; diff --git a/src/components/BulkEdit/AddTagPanel.tsx b/src/components/BulkEdit/AddTagPanel.tsx index 4752c65a..5459dcc6 100644 --- a/src/components/BulkEdit/AddTagPanel.tsx +++ b/src/components/BulkEdit/AddTagPanel.tsx @@ -1,9 +1,3 @@ -// this engages a babel macro that does cool emotion stuff (like source maps). See https://emotion.sh/docs/babel-macros -import css from "@emotion/css/macro"; -// these two lines make the css prop work on react elements -import { jsx } from "@emotion/core"; -/** @jsx jsx */ - import React from "react"; import { IFilter } from "../../IFilter"; import { observer } from "mobx-react"; @@ -15,7 +9,7 @@ export const AddTagPanel: React.FunctionComponent<{ filterHolder: FilterHolder; refresh: () => void; backgroundColor: string; -}> = observer(props => { +}> = observer((props) => { return ( <BulkEditPanel panelLabel="Add Tag" diff --git a/src/components/BulkEdit/AssignPublisherPanel.tsx b/src/components/BulkEdit/AssignPublisherPanel.tsx index 979938f1..59e1095e 100644 --- a/src/components/BulkEdit/AssignPublisherPanel.tsx +++ b/src/components/BulkEdit/AssignPublisherPanel.tsx @@ -1,9 +1,3 @@ -// this engages a babel macro that does cool emotion stuff (like source maps). See https://emotion.sh/docs/babel-macros -import css from "@emotion/css/macro"; -// these two lines make the css prop work on react elements -import { jsx } from "@emotion/core"; -/** @jsx jsx */ - import React from "react"; import { IFilter } from "../../IFilter"; import { observer } from "mobx-react"; @@ -15,7 +9,7 @@ export const AssignPublisherPanel: React.FunctionComponent<{ filterHolder: FilterHolder; backgroundColor: string; refresh: () => void; -}> = observer(props => { +}> = observer((props) => { return ( <BulkEditPanel panelLabel="Change Publisher" diff --git a/src/components/BulkEdit/BulkEditPanel.tsx b/src/components/BulkEdit/BulkEditPanel.tsx index 17406f3b..c40f04b7 100644 --- a/src/components/BulkEdit/BulkEditPanel.tsx +++ b/src/components/BulkEdit/BulkEditPanel.tsx @@ -11,7 +11,7 @@ import { Checkbox, FormControlLabel, Select, - MenuItem + MenuItem, } from "@material-ui/core"; import { IFilter } from "../../IFilter"; @@ -33,7 +33,7 @@ export const BulkEditPanel: React.FunctionComponent<{ ) => void; filterHolder: FilterHolder; refresh: () => void; -}> = observer(props => { +}> = observer((props) => { const [valueToSet, setValueToSet] = useState<string | undefined>(""); const [armed, setArmed] = useState(false); const user = useGetLoggedInUser(); @@ -44,6 +44,8 @@ export const BulkEditPanel: React.FunctionComponent<{ const notFilteredYet = !( !!props.filterHolder.completeFilter.bookshelf || !!props.filterHolder.completeFilter.language || + !!props.filterHolder.completeFilter.publisher || + !!props.filterHolder.completeFilter.originalPublisher || // lots of other fields, e.g. copyright, end up as part of search (e.g. search:"copyright:foo") !!props.filterHolder.completeFilter.search ); @@ -78,7 +80,7 @@ export const BulkEditPanel: React.FunctionComponent<{ control={ <Checkbox checked={armed} - onChange={e => { + onChange={(e) => { setArmed(e.target.checked); }} /> @@ -101,11 +103,11 @@ export const BulkEditPanel: React.FunctionComponent<{ css={css` width: 400px; `} - onChange={e => { + onChange={(e) => { setValueToSet(e.target.value as string); }} > - {props.choices.map(c => ( + {props.choices.map((c) => ( <MenuItem key={c} value={c}> {c} </MenuItem> @@ -121,7 +123,7 @@ export const BulkEditPanel: React.FunctionComponent<{ width: 600px; `} defaultValue={valueToSet} - onChange={evt => { + onChange={(evt) => { const v = evt.target.value.trim(); setValueToSet(v.length ? v : undefined); }} diff --git a/src/components/Grid/GridColumns.tsx b/src/components/Grid/GridColumns.tsx index 70925783..17f674f8 100644 --- a/src/components/Grid/GridColumns.tsx +++ b/src/components/Grid/GridColumns.tsx @@ -7,7 +7,7 @@ import { jsx } from "@emotion/core"; import React, { useState, FunctionComponent } from "react"; import { Column as DevExpressColumn, - TableFilterRow + TableFilterRow, } from "@devexpress/dx-react-grid"; import { Checkbox, Link, TableCell, Select, MenuItem } from "@material-ui/core"; @@ -52,17 +52,18 @@ export function getBookGridColumnsDefinitions(): IGridColumn[] { addToFilter: (filter: IFilter, value: string) => { // enhance: at the moment we don't have a "title:" search axis, so this will search other fields as well filter.search = value; - } + }, }, { name: "languages", title: "Languages", defaultVisible: true, - getCellValue: (b: Book) => b.languages.map(l => l.name).join(", "), + getCellValue: (b: Book) => + b.languages.map((l) => l.name).join(", "), addToFilter: (filter: IFilter, value: string) => { // enhance: at the moment we don't have a "language:" search axis, so this will search other fields as well filter.search = value; - } + }, }, { name: "tags", @@ -70,15 +71,15 @@ export function getBookGridColumnsDefinitions(): IGridColumn[] { getCellValue: (b: Book) => b.tags .filter( - t => - !kTagsToFilterOutOfTagsList.find(tagToFilterOut => + (t) => + !kTagsToFilterOutOfTagsList.find((tagToFilterOut) => t.startsWith(tagToFilterOut) ) ) .join(", "), addToFilter: (filter: IFilter, value: string) => { filter.otherTags = value; - } + }, }, { name: "bookshelves", @@ -87,7 +88,7 @@ export function getBookGridColumnsDefinitions(): IGridColumn[] { getCellValue: (b: Book) => b.bookshelves.join(","), addToFilter: (filter: IFilter, value: string) => { filter.bookshelf = value; - } + }, }, { name: "incoming", @@ -100,7 +101,7 @@ export function getBookGridColumnsDefinitions(): IGridColumn[] { <TagExistsFilterCell {...props} /> ), addToFilter: (filter: IFilter, value: string) => - updateFilterForExistenceOfTag("system:Incoming", filter, value) + updateFilterForExistenceOfTag("system:Incoming", filter, value), }, { name: "level", @@ -115,22 +116,22 @@ export function getBookGridColumnsDefinitions(): IGridColumn[] { // TODO: we need a way to query to for a missing level indicator addToFilter: (filter: IFilter, value: string) => { filter.search += ` level:${titleCase(value)}`; - } + }, }, { name: "topic", defaultVisible: true, getCellValue: (b: Book) => b.tags - .filter(t => t.startsWith("topic:")) - .map(t => ( + .filter((t) => t.startsWith("topic:")) + .map((t) => ( <GridSearchLink key={t} search={t}> {t.replace(/topic:/, "")} </GridSearchLink> )), addToFilter: (filter: IFilter, value: string) => { filter.topic = titleCase(value); - } + }, }, { name: "harvestState", @@ -143,11 +144,11 @@ export function getBookGridColumnsDefinitions(): IGridColumn[] { choices={["", "Done", "Failed", "New", "Updated"]} {...props} /> - ) + ), }, { name: "harvestLog", - defaultVisible: false + defaultVisible: false, }, { name: "inCirculation", @@ -162,7 +163,7 @@ export function getBookGridColumnsDefinitions(): IGridColumn[] { if (value === "Yes") filter.inCirculation = InCirculationOptions.Yes; // otherwise don't mention it - } + }, }, { name: "license", sortingEnabled: true }, { @@ -171,12 +172,24 @@ export function getBookGridColumnsDefinitions(): IGridColumn[] { addToFilter: (filter: IFilter, value: string) => { filter.search = `copyright:${value} ` + (filter.search || ""); - } + }, }, { name: "pageCount", sortingEnabled: true }, { name: "createdAt", sortingEnabled: true }, - { name: "publisher", sortingEnabled: true }, - { name: "originalPublisher", sortingEnabled: true }, + { + name: "publisher", + sortingEnabled: true, + addToFilter: (filter: IFilter, value: string) => { + filter.publisher = value; + }, + }, + { + name: "originalPublisher", + sortingEnabled: true, + addToFilter: (filter: IFilter, value: string) => { + filter.originalPublisher = value; + }, + }, { name: "uploader", sortingEnabled: true, @@ -192,15 +205,15 @@ export function getBookGridColumnsDefinitions(): IGridColumn[] { ), addToFilter: (filter: IFilter, value: string) => { filter.search = `uploader:${value} ` + (filter.search || ""); - } - } + }, + }, ]; // generate the capitalized column names since the grid doesn't do that. return ( definitions //.sort((a, b) => a.name.localeCompare(b.name)) - .map(c => { + .map((c) => { const x = { ...c }; if (c.title === undefined) { x.title = titleCase(c.name); @@ -212,13 +225,13 @@ export function getBookGridColumnsDefinitions(): IGridColumn[] { export const GridSearchLink: React.FunctionComponent<{ search: string; -}> = props => { +}> = (props) => { const location = { title: props.search, pageType: "grid", filter: { - search: props.search - } + search: props.search, + }, }; const url = "/grid/?" + QueryString.stringify(location); return ( @@ -236,12 +249,12 @@ export const GridSearchLink: React.FunctionComponent<{ const TagCheckbox: React.FunctionComponent<{ book: Book; tag: string; -}> = props => { +}> = (props) => { const [present, setPresent] = useState(props.book.tags.includes(props.tag)); return ( <Checkbox checked={present} - onChange={e => { + onChange={(e) => { props.book.setBooleanTag(props.tag, e.target.checked); props.book.saveAdminDataToParse(); setPresent(e.target.checked); @@ -251,9 +264,11 @@ const TagCheckbox: React.FunctionComponent<{ }; // eslint-disable-next-line @typescript-eslint/no-unused-vars -const ChoicesFilterCell: React.FunctionComponent<TableFilterRow.CellProps & { - choices: string[]; -}> = props => { +const ChoicesFilterCell: React.FunctionComponent< + TableFilterRow.CellProps & { + choices: string[]; + } +> = (props) => { const [value, setValue] = useState(props.filter?.value || ""); return ( <TableCell> @@ -273,7 +288,7 @@ const ChoicesFilterCell: React.FunctionComponent<TableFilterRow.CellProps & { id="demo-simple-select" color="secondary" //<--- doesn't work displayEmpty={true} - renderValue={v => { + renderValue={(v) => { const x = v as string; return <div>{x ? x : "All"}</div>; }} @@ -281,16 +296,16 @@ const ChoicesFilterCell: React.FunctionComponent<TableFilterRow.CellProps & { css={css` font-size: 0.875rem !important; `} - onChange={e => { + onChange={(e) => { setValue(e.target.value as string); props.onFilter({ columnName: props.column.name, operation: "contains", - value: e.target.value as string + value: e.target.value as string, }); }} > - {props.choices.map(c => ( + {props.choices.map((c) => ( <MenuItem key={c} value={c}> {c.length === 0 ? "All" : c} </MenuItem> @@ -301,7 +316,9 @@ const ChoicesFilterCell: React.FunctionComponent<TableFilterRow.CellProps & { }; // shows a checkbox in the filter row; ticking the box leads to a call to the gridColumn definition `addToFilter()` -const TagExistsFilterCell: React.FunctionComponent<TableFilterRow.CellProps> = props => { +const TagExistsFilterCell: React.FunctionComponent<TableFilterRow.CellProps> = ( + props +) => { const [checked, setChecked] = useState( props.filter?.value === "true" || false ); @@ -312,12 +329,12 @@ const TagExistsFilterCell: React.FunctionComponent<TableFilterRow.CellProps> = p padding-left: 0; `} checked={checked} - onChange={e => { + onChange={(e) => { props.onFilter({ columnName: props.column.name, operation: "contains", // we're switching to the opposite of what `checked` was - value: !checked ? "true" : "false" + value: !checked ? "true" : "false", }); setChecked(!checked); }} diff --git a/src/components/Grid/GridControlInternal.tsx b/src/components/Grid/GridControlInternal.tsx index 81fdc680..b4a773ad 100644 --- a/src/components/Grid/GridControlInternal.tsx +++ b/src/components/Grid/GridControlInternal.tsx @@ -171,7 +171,7 @@ const GridControlInternal: React.FunctionComponent<IGridControlProps> = observer ) ); //setColumnNamesInDisplayOrder(bookGridColumns.map(c => c.name)); - }, [router, user, user?.moderator, bookGridColumnDefinitions]); + }, [router, user, bookGridColumnDefinitions]); // note: this is an embedded function as a way to get at bookGridColumnDefinitions. It's important // that we don't reconstruct it on every render, or else we'll lose cursor focus on each key press. diff --git a/src/components/HomePage.tsx b/src/components/HomePage.tsx index 6fdd6882..4618dfd8 100644 --- a/src/components/HomePage.tsx +++ b/src/components/HomePage.tsx @@ -7,10 +7,11 @@ import { HomeBanner } from "./banners/HomeBanner"; import { ListOfBookGroups } from "./ListOfBookGroups"; import { FeatureGroup } from "./FeatureGroup"; import { SpecialInterestGroup } from "./SpecialInterestsGroup"; +import { PublisherGroup } from "./PublisherGroup"; export const HomePage: React.FunctionComponent = () => { const almostAllBooksFilter: IFilter = { - inCirculation: InCirculationOptions.Yes + inCirculation: InCirculationOptions.Yes, }; return ( <> @@ -31,10 +32,7 @@ export const HomePage: React.FunctionComponent = () => { order={"-createdAt"} /> - <BookshelfGroup - title="Publishers" - bookShelfCategory="publisher" - /> + <PublisherGroup /> <FeatureGroup title="Book Features" /> diff --git a/src/components/PublisherGroup.tsx b/src/components/PublisherGroup.tsx new file mode 100644 index 00000000..9a070b9f --- /dev/null +++ b/src/components/PublisherGroup.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import CategoryCard from "./CategoryCard"; +import { CategoryCardGroup } from "./CategoryCardGroup"; + +const encodeUrl = require("encodeurl"); + +// Normally the publisher name matches the image name, but if not we change it here: +const nameToImageMap = new Map<string, string>([ + // // something in our pipeline won't deliver an image that starts with "3" + ["3Asafeer", "Asafeer"], + ["Room To Read", "Room to Read"], +]); + +export const PublisherGroup: React.FunctionComponent<{}> = (props) => { + // enhance: once we get the Publisher field filled in, we can switch to getting a full list of publishers + // and then us the following group + const publishers = [ + "African Storybook", + "3Asafeer", + "Book Dash", + "Little Zebra Books", + "Pratham", + "Room To Read", + ]; + const cards = publishers.sort().map((publisher) => { + const imageName = nameToImageMap.get(publisher) ?? publisher; + return ( + <CategoryCard + key={publisher} + //preTitle={publisher} + title={publisher} + bookCount="??" + filter={{ + publisher, + }} + pageType={"project"} + img={ + "https://share.bloomlibrary.org/bookshelf-images/" + + encodeUrl(imageName) + + ".png" + } + /> + ); + }); + + return ( + <CategoryCardGroup title="Publishers" {...props}> + {cards} + </CategoryCardGroup> + ); +}; diff --git a/src/components/PublisherPages.tsx b/src/components/PublisherPages.tsx index 467b8b6a..3ad90eea 100644 --- a/src/components/PublisherPages.tsx +++ b/src/components/PublisherPages.tsx @@ -66,6 +66,34 @@ export const AsafeerPage: React.FunctionComponent = () => { ); }; +export const LittleZebraPage: React.FunctionComponent = () => { + const filter = { publisher: "Little Zebra" }; + const description = ( + <React.Fragment> + <ExternalLink href="https://www.littlezebrabooks.com/"> + Little Zebra Books{" "} + </ExternalLink> + is a Christian non-profit organization that exists to serve + African communities through the development, production, and + distribution of African-language books. + </React.Fragment> + ); + return ( + <div> + <PublisherBanner + title="Little Zebra" + showTitle={false} + filter={filter} + logoUrl={`https://share.bloomlibrary.org/bookshelf-images/LittleZebra.png`} + collectionDescription={description} + /> + + <ListOfBookGroups> + <StandardPublisherGroups filter={filter} /> + </ListOfBookGroups> + </div> + ); +}; export const PrathamPage: React.FunctionComponent = () => { const filter = { bookshelf: "Pratham" }; const description = ( diff --git a/src/connection/LibraryQueryHooks.ts b/src/connection/LibraryQueryHooks.ts index 6acb4030..783e98eb 100644 --- a/src/connection/LibraryQueryHooks.ts +++ b/src/connection/LibraryQueryHooks.ts @@ -674,6 +674,14 @@ export function constructParseBookQuery( delete params.where.feature; params.where.features = f.feature; //my understanding is that this means it just has to contain this, could have others } + if (f.publisher) { + delete params.where.publisher; + params.where.publisher = f.publisher; + } + if (f.originalPublisher) { + delete params.where.originalPublisher; + params.where.publisher = f.originalPublisher; + } delete params.where.inCirculation; switch (f.inCirculation) { case undefined: