Skip to content

Commit 1d03bcf

Browse files
authored
Add confirmation when disassociating collection libraries (PP-2408) (#169)
## Description Adds a confirmation dialog when disassociating a library from a collection. ## Motivation and Context When a library is disassociated from a collection, all patron loans and holds are immediately removed when the change is saved. The confirmation reduces the risk of accidental disassociation by reminding the user of the repercussions of the action before it is effected. ## How Has This Been Tested? - Manually tested locally. - New tests. - All tests pass locally. - [CI tests](https://github.com/ThePalaceProject/circulation-admin/actions/runs/17078864736) pass. ## Checklist: - N/A - I have updated the documentation accordingly. - [x] All new and existing tests passed.
1 parent bbe2c23 commit 1d03bcf

File tree

2 files changed

+94
-6
lines changed

2 files changed

+94
-6
lines changed

src/components/Collections.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import {
1212
CollectionsData,
1313
CollectionData,
1414
LibraryData,
15+
LibraryWithSettingsData,
1516
LibraryRegistrationsData,
16-
ServiceData,
1717
} from "../interfaces";
1818
import ServiceWithRegistrationsEditForm from "./ServiceWithRegistrationsEditForm";
1919
import TrashIcon from "./icons/TrashIcon";
@@ -36,7 +36,24 @@ export interface CollectionsProps
3636

3737
export class CollectionEditForm extends ServiceWithRegistrationsEditForm<
3838
CollectionsData
39-
> {}
39+
> {
40+
/**
41+
* Override to display a confirmation message before removing a library
42+
* association. We display the confirmation and, if successful, call the
43+
* superclass method to actually remove the library from our state.
44+
* @param library
45+
*/
46+
removeLibrary(library: LibraryWithSettingsData) {
47+
const libraryData = this.getLibrary(library.short_name);
48+
const libraryName = libraryData ? libraryData.name : library.short_name;
49+
const confirmationMessage =
50+
`Disassociating library "${libraryName}" from this collection will ` +
51+
"remove all loans and holds for its patrons. Do you wish to continue?";
52+
if (window.confirm(confirmationMessage)) {
53+
super.removeLibrary(library);
54+
}
55+
}
56+
}
4057

4158
/**
4259
* Right panel for collections on the system configuration page.

src/components/__tests__/Collections-test.tsx

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { expect } from "chai";
2+
import * as sinon from "sinon";
23
import { stub } from "sinon";
34
import * as React from "react";
4-
import { shallow, mount } from "enzyme";
5+
import { ReactWrapper, mount } from "enzyme";
56
import * as PropTypes from "prop-types";
6-
7+
import { ProtocolData } from "../../interfaces";
78
import Admin from "../../models/Admin";
8-
import { Collections } from "../Collections";
9+
import { Collections, CollectionEditForm } from "../Collections";
10+
import buildStore from "../../store";
911

1012
const collections = [
1113
{
@@ -27,7 +29,7 @@ const collections = [
2729
name: "RBDigital",
2830
},
2931
];
30-
import buildStore from "../../store";
32+
const protocols: ProtocolData[] = [{ name: "test protocol", settings: [] }];
3133

3234
describe("Collections", () => {
3335
let wrapper;
@@ -127,5 +129,74 @@ describe("Collections", () => {
127129
expect(deletedCollection.find("a.edit-item").length).to.equal(0);
128130
expect(deletedCollection.find("button.delete-item").length).to.equal(0);
129131
});
132+
133+
describe("confirm before disassociating libraries", () => {
134+
let wrapper: ReactWrapper;
135+
let confirmStub: sinon.SinonStub;
136+
137+
const initialLibraries = [
138+
{ short_name: "palace", name: "Palace" },
139+
{ short_name: "another-library", name: "Another Library" },
140+
] as const;
141+
const collection = {
142+
id: 7,
143+
name: "An OPDS Collection",
144+
protocol: "OPDS Import",
145+
146+
libraries: [...initialLibraries],
147+
};
148+
149+
beforeEach(() => {
150+
confirmStub = sinon.stub(window, "confirm");
151+
152+
wrapper = mount(
153+
<CollectionEditForm
154+
disabled={false}
155+
data={{
156+
collections: [collection],
157+
protocols: [],
158+
allLibraries: [...initialLibraries],
159+
}}
160+
item={collection}
161+
urlBase="/collections"
162+
listDataKey="collections"
163+
/>
164+
);
165+
});
166+
167+
afterEach(() => {
168+
confirmStub.restore();
169+
});
170+
171+
it("calls window.confirm when delete button is clicked", () => {
172+
confirmStub.returns(false);
173+
174+
// The confirmation dialog should not be invoked before we click.
175+
expect(confirmStub.calledOnce).to.be.false;
176+
177+
wrapper.find("button.remove-btn").at(0).simulate("click");
178+
expect(confirmStub.calledOnce).to.be.true;
179+
expect(confirmStub.firstCall.args.length).to.equal(1);
180+
const message: string = confirmStub.firstCall.args[0];
181+
expect(message).to.equal(
182+
'Disassociating library "Palace" from this collection will ' +
183+
"remove all loans and holds for its patrons. Do you wish to continue?"
184+
);
185+
});
186+
187+
it("does not delete library if confirmation is canceled", () => {
188+
confirmStub.returns(false);
189+
wrapper.find("button.remove-btn").at(0).simulate("click");
190+
// We didn't delete, so we should still have the originals.
191+
expect(wrapper.state("libraries")).to.deep.equal(initialLibraries);
192+
});
193+
194+
it("deletes library if confirmation is accepted", () => {
195+
confirmStub.returns(true);
196+
wrapper.find("button.remove-btn").at(0).simulate("click");
197+
// We deleted the first library, so it should be gone from the state.
198+
expect(wrapper.state("libraries")).to.deep.equal([initialLibraries[1]]);
199+
});
200+
});
130201
});
131202
});

0 commit comments

Comments
 (0)