Skip to content

Commit

Permalink
Implement global announcements. (#41)
Browse files Browse the repository at this point in the history
* Add sitewide announcements config tab.

* Fix tests.

* Add tests.

* Update changelog.
  • Loading branch information
ray-lee authored Sep 1, 2022
1 parent b68a32b commit eda1994
Show file tree
Hide file tree
Showing 13 changed files with 394 additions and 2 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
## Changelog

### v0.2.1
### v0.3.0

#### Fixed

- The selected media type and sort order are now correctly saved with auto updating lists.

#### Updated

- Added Sitewide Announcements configuration tab.

### v0.2.0

#### Updated
Expand Down
55 changes: 55 additions & 0 deletions src/__tests__/actions-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -982,4 +982,59 @@ describe("actions", () => {
expect(data).to.eql(customListDetailsData);
});
});

describe("fetchSitewideAnnouncements", () => {
it("dispatches request, load, and success", async () => {
const dispatch = stub();
const sitewideAnnouncementsData = "sitewide announcements";
fetcher.testData = {
ok: true,
status: 200,
json: () =>
new Promise<any>((resolve) => {
resolve(sitewideAnnouncementsData);
}),
};
fetcher.resolve = true;

const data = await actions.fetchSitewideAnnouncements()(dispatch);
expect(dispatch.callCount).to.equal(3);
expect(dispatch.args[0][0].type).to.equal(
`${ActionCreator.SITEWIDE_ANNOUNCEMENTS}_${ActionCreator.REQUEST}`
);
expect(dispatch.args[0][0].url).to.equal("/admin/announcements");
expect(dispatch.args[1][0].type).to.equal(
`${ActionCreator.SITEWIDE_ANNOUNCEMENTS}_${ActionCreator.SUCCESS}`
);
expect(dispatch.args[2][0].type).to.equal(
`${ActionCreator.SITEWIDE_ANNOUNCEMENTS}_${ActionCreator.LOAD}`
);
expect(data).to.deep.equal(sitewideAnnouncementsData);
});
});

describe("editSitewideAnnouncements", () => {
it("dispatches request and success", async () => {
const editSitewideAnnouncementsUrl = "/admin/announcements";
const dispatch = stub();
const formData = new (window as any).FormData();
formData.append("announcements", "[]");

fetchMock.mock(editSitewideAnnouncementsUrl, "server response");
const fetchArgs = fetchMock.calls();

await actions.editSitewideAnnouncements(formData)(dispatch);
expect(dispatch.callCount).to.equal(3);
expect(dispatch.args[0][0].type).to.equal(
`${ActionCreator.EDIT_SITEWIDE_ANNOUNCEMENTS}_${ActionCreator.REQUEST}`
);
expect(dispatch.args[1][0].type).to.equal(
`${ActionCreator.EDIT_SITEWIDE_ANNOUNCEMENTS}_${ActionCreator.SUCCESS}`
);
expect(fetchMock.called()).to.equal(true);
expect(fetchArgs[0][0]).to.equal(editSitewideAnnouncementsUrl);
expect(fetchArgs[0][1].method).to.equal("POST");
expect(fetchArgs[0][1].body).to.equal(formData);
});
});
});
20 changes: 20 additions & 0 deletions src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
PatronData,
DiagnosticsData,
FeatureFlags,
SitewideAnnouncementsData,
} from "./interfaces";
import { CollectionData } from "opds-web-client/lib/interfaces";
import DataFetcher from "opds-web-client/lib/DataFetcher";
Expand Down Expand Up @@ -81,6 +82,8 @@ export default class ActionCreator extends BaseActionCreator {
static readonly PATRON_AUTH_SERVICES = "PATRON_AUTH_SERVICES";
static readonly EDIT_PATRON_AUTH_SERVICE = "EDIT_PATRON_AUTH_SERVICE";
static readonly DELETE_PATRON_AUTH_SERVICE = "DELETE_PATRON_AUTH_SERVICE";
static readonly SITEWIDE_ANNOUNCEMENTS = "SITEWIDE_ANNOUNCEMENTS";
static readonly EDIT_SITEWIDE_ANNOUNCEMENTS = "EDIT_SITEWIDE_ANNOUNCEMENTS";
static readonly SITEWIDE_SETTINGS = "SITEWIDE_SETTINGS";
static readonly EDIT_SITEWIDE_SETTING = "EDIT_SITEWIDE_SETTING";
static readonly DELETE_SITEWIDE_SETTING = "DELETE_SITEWIDE_SETTING";
Expand Down Expand Up @@ -1154,6 +1157,23 @@ export default class ActionCreator extends BaseActionCreator {
);
}

fetchSitewideAnnouncements() {
const url = "/admin/announcements";
return this.fetchJSON<SitewideAnnouncementsData>(
ActionCreator.SITEWIDE_ANNOUNCEMENTS,
url
).bind(this);
}

editSitewideAnnouncements(data: FormData) {
const url = "/admin/announcements";
return this.postForm(
ActionCreator.EDIT_SITEWIDE_ANNOUNCEMENTS,
url,
data
).bind(this);
}

setFeatureFlags(featureFlags: FeatureFlags) {
return {
type: ActionCreator.SET_FEATURE_FLAGS,
Expand Down
1 change: 1 addition & 0 deletions src/components/AnnouncementForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export default class AnnouncementForm extends React.Component<
/>
<Button
callback={(e: Event) => this.add(e)}
content="Add"
className="inline left-align"
disabled={shouldDisable()}
/>
Expand Down
3 changes: 3 additions & 0 deletions src/components/ConfigTabContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Collections from "./Collections";
import AdminAuthServices from "./AdminAuthServices";
import IndividualAdmins from "./IndividualAdmins";
import PatronAuthServices from "./PatronAuthServices";
import SitewideAnnouncements from "./SitewideAnnouncements";
import SitewideSettings from "./SitewideSettings";
import MetadataServices from "./MetadataServices";
import AnalyticsServices from "./AnalyticsServices";
Expand Down Expand Up @@ -58,6 +59,7 @@ export default class ConfigTabContainer extends TabContainer<
storage: StorageServices,
catalogServices: CatalogServices,
discovery: DiscoveryServices,
sitewideAnnouncements: SitewideAnnouncements,
};

LIBRARIAN_TABS = ["libraries"];
Expand All @@ -68,6 +70,7 @@ export default class ConfigTabContainer extends TabContainer<
adminAuth: "Admin Authentication",
individualAdmins: "Admins",
patronAuth: "Patron Authentication",
sitewideAnnouncements: "Sitewide Announcements",
sitewideSettings: "Sitewide Settings",
cdn: "CDN",
catalogServices: "External Catalogs",
Expand Down
126 changes: 126 additions & 0 deletions src/components/SitewideAnnouncements.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import * as React from "react";
import { connect } from "react-redux";
import { Alert } from "react-bootstrap";
import { Form } from "library-simplified-reusable-components";
import LoadingIndicator from "opds-web-client/lib/components/LoadingIndicator";
import ActionCreator from "../actions";
import { SitewideAnnouncementsData, AnnouncementData } from "../interfaces";
import EditableConfigList, {
EditableConfigListStateProps,
EditableConfigListDispatchProps,
EditableConfigListOwnProps,
EditableConfigListProps,
} from "./EditableConfigList";
import ErrorMessage from "./ErrorMessage";
import AnnouncementsSection from "./AnnouncementsSection";

/** Right panel for sitewide announcements on the system configuration page. */
export class SitewideAnnouncements extends EditableConfigList<
SitewideAnnouncementsData,
AnnouncementData
> {
// There is no individual item edit form for an announcement.
EditForm = null;
listDataKey = "announcements";
itemTypeName = "sitewide announcement";
urlBase = "/admin/web/config/sitewideAnnouncements/";
identifierKey = "id";
labelKey = "content";

private announcementsRef = React.createRef<AnnouncementsSection>();

constructor(props: EditableConfigListProps<SitewideAnnouncementsData>) {
super(props);

this.submit = this.submit.bind(this);
}

render() {
const {
data,
editOrCreate,
fetchError,
formError,
isFetching,
responseBody,
} = this.props;

const headers = this.getHeaders();

const canListAllData =
!isFetching && !editOrCreate && data?.[this.listDataKey];

const canEdit = this.canEdit(data?.settings?.[0] || {});

return (
<div className={this.getClassName()}>
<h2>{headers["h2"]}</h2>
{canListAllData && this.links?.["info"] && (
<Alert bsStyle="info">{this.links["info"]}</Alert>
)}
{responseBody && (
<Alert bsStyle="success">{this.successMessage()}</Alert>
)}
{fetchError && <ErrorMessage error={fetchError} />}
{formError && <ErrorMessage error={formError} />}
{isFetching && <LoadingIndicator />}

{canListAllData && (
<Form
onSubmit={this.submit}
className="no-border edit-form"
disableButton={!canEdit || isFetching}
content={[
<AnnouncementsSection
key="announcements-section"
setting={data.settings[0]}
value={data[this.listDataKey]}
ref={this.announcementsRef}
/>,
]}
/>
)}
</div>
);
}

async submit(data: FormData) {
const announcements = this.announcementsRef.current.getValue();

data?.set("announcements", JSON.stringify(announcements));

await this.editItem(data);
}
}

function mapStateToProps(state) {
return {
data: state.editor.sitewideAnnouncements.data,
responseBody: state.editor.sitewideAnnouncements.successMessage,
fetchError: state.editor.sitewideAnnouncements.fetchError,
formError: state.editor.sitewideAnnouncements.formError,
isFetching:
state.editor.sitewideAnnouncements.isFetching ||
state.editor.sitewideAnnouncements.isEditing,
};
}

function mapDispatchToProps(dispatch, ownProps) {
const actions = new ActionCreator(null, ownProps.csrfToken);
return {
fetchData: () => dispatch(actions.fetchSitewideAnnouncements()),
editItem: (data: FormData) =>
dispatch(actions.editSitewideAnnouncements(data)),
};
}

const ConnectedSitewideAnnouncements = connect<
EditableConfigListStateProps<SitewideAnnouncementsData>,
EditableConfigListDispatchProps<SitewideAnnouncementsData>,
EditableConfigListOwnProps
>(
mapStateToProps,
mapDispatchToProps
)(SitewideAnnouncements);

export default ConnectedSitewideAnnouncements;
2 changes: 1 addition & 1 deletion src/components/__tests__/AnnouncementForm-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe("AnnouncementForm", () => {
it("renders the buttons", () => {
let buttons = wrapper.find("button");
expect(buttons.length).to.equal(2);
expect(buttons.at(0).text()).to.equal("Submit");
expect(buttons.at(0).text()).to.equal("Add");
expect(buttons.at(1).text()).to.equal("Cancel");
});
it("keeps track of whether the content is too short or too long", () => {
Expand Down
5 changes: 5 additions & 0 deletions src/components/__tests__/ConfigTabContainer-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import SearchServices from "../SearchServices";
import StorageServices from "../StorageServices";
import CatalogServices from "../CatalogServices";
import DiscoveryServices from "../DiscoveryServices";
import SitewideAnnouncements from "../SitewideAnnouncements";
import { mockRouterContext } from "./routing";
import Admin from "../../models/Admin";

Expand Down Expand Up @@ -62,6 +63,7 @@ describe("ConfigTabContainer", () => {
expect(linkTexts).to.contain("Metadata");
expect(linkTexts).to.contain("CDN");
expect(linkTexts).to.contain("External Catalogs");
expect(linkTexts).to.contain("Sitewide Announcements");
});

it("shows components", () => {
Expand All @@ -79,6 +81,7 @@ describe("ConfigTabContainer", () => {
StorageServices,
CatalogServices,
DiscoveryServices,
SitewideAnnouncements,
];
for (const componentClass of componentClasses) {
const component = wrapper.find(componentClass);
Expand Down Expand Up @@ -152,6 +155,7 @@ describe("ConfigTabContainer", () => {
CatalogServices,
DiscoveryServices,
AnalyticsServices,
SitewideAnnouncements,
];
for (const componentClass of hiddenComponentClasses) {
const component = wrapper.find(componentClass);
Expand Down Expand Up @@ -206,6 +210,7 @@ describe("ConfigTabContainer", () => {
CatalogServices,
DiscoveryServices,
AnalyticsServices,
SitewideAnnouncements,
];
for (const componentClass of hiddenComponentClasses) {
const component = wrapper.find(componentClass);
Expand Down
Loading

0 comments on commit eda1994

Please sign in to comment.