@thepalaceproject/circulation-admin 1.44.0 → 1.44.1-post.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -139,5 +139,5 @@
139
139
  "*.{js,jsx,ts,tsx,css,md}": "prettier --write",
140
140
  "*.{js,css,md}": "prettier --write"
141
141
  },
142
- "version": "1.44.0"
142
+ "version": "1.44.1-post.2"
143
143
  }
@@ -156,8 +156,8 @@ describe("Collections - associated library disclosure", () => {
156
156
 
157
157
  const items = container.querySelectorAll(".associated-items li");
158
158
  // Sorted alphabetically: Alpha, Beta
159
- expect(items[0].textContent).toBe("Alpha Library");
160
- expect(items[1].textContent).toBe("Beta Library");
159
+ expect(items[0].textContent).toBe("Alpha Library - alpha");
160
+ expect(items[1].textContent).toBe("Beta Library - beta");
161
161
  });
162
162
 
163
163
  it("links a library name to its config page when a uuid is available", () => {
@@ -177,7 +177,7 @@ describe("Collections - associated library disclosure", () => {
177
177
  ".associated-items a"
178
178
  );
179
179
  expect(link).not.toBeNull();
180
- expect(link.textContent).toBe("Alpha Library");
180
+ expect(link.textContent).toBe("Alpha Library - alpha");
181
181
  expect(link.href).toContain("/admin/web/config/libraries/edit/uuid-alpha");
182
182
  });
183
183
 
@@ -196,7 +196,7 @@ describe("Collections - associated library disclosure", () => {
196
196
 
197
197
  expect(container.querySelector(".associated-items a")).toBeNull();
198
198
  expect(container.querySelector(".associated-items li").textContent).toBe(
199
- "Delta Library"
199
+ "Delta Library - delta"
200
200
  );
201
201
  });
202
202
 
@@ -215,7 +215,9 @@ describe("DiscoveryServices - registered library disclosure", () => {
215
215
  fireEvent.click(container.querySelector(".association-toggle"));
216
216
 
217
217
  const item = container.querySelector(".associated-items li");
218
- expect(item.textContent).toBe("Alpha Library - registered - production");
218
+ expect(item.textContent).toBe(
219
+ "Alpha Library - alpha - registered - production"
220
+ );
219
221
  });
220
222
 
221
223
  it("shows '- registered' without a stage when stage is absent", () => {
@@ -231,7 +233,7 @@ describe("DiscoveryServices - registered library disclosure", () => {
231
233
  fireEvent.click(container.querySelector(".association-toggle"));
232
234
 
233
235
  const item = container.querySelector(".associated-items li");
234
- expect(item.textContent).toBe("Alpha Library - registered");
236
+ expect(item.textContent).toBe("Alpha Library - alpha - registered");
235
237
  });
236
238
 
237
239
  // ── Sorting ───────────────────────────────────────────────────────────────
@@ -290,7 +292,7 @@ describe("DiscoveryServices - registered library disclosure", () => {
290
292
  ".associated-items a"
291
293
  );
292
294
  expect(link).not.toBeNull();
293
- expect(link.textContent).toBe("Alpha Library");
295
+ expect(link.textContent).toBe("Alpha Library - alpha");
294
296
  expect(link.href).toContain("/admin/web/config/libraries/edit/uuid-alpha");
295
297
  // The suffix should not be inside the link.
296
298
  expect(link.nextSibling.textContent).toBe(" - registered - production");
@@ -312,8 +314,36 @@ describe("DiscoveryServices - registered library disclosure", () => {
312
314
 
313
315
  expect(container.querySelector(".associated-items a")).toBeNull();
314
316
  expect(container.querySelector(".associated-items li").textContent).toBe(
315
- "Delta Library - registered - testing"
317
+ "Delta Library - delta - registered - testing"
318
+ );
319
+ });
320
+
321
+ it("falls back to the registration record's own name and uuid for a library missing from allLibraries", () => {
322
+ const { container } = renderServices({
323
+ discovery_services: [{ id: 1, protocol: "p", name: "Service A" } as any],
324
+ libraryRegistrations: [
325
+ {
326
+ id: 1,
327
+ libraries: [
328
+ {
329
+ short_name: "omega",
330
+ name: "Omega Library",
331
+ uuid: "uuid-omega",
332
+ status: "success",
333
+ stage: "testing",
334
+ } as any,
335
+ ],
336
+ },
337
+ ],
338
+ });
339
+ fireEvent.click(container.querySelector(".association-toggle"));
340
+
341
+ const link = container.querySelector<HTMLAnchorElement>(
342
+ ".associated-items a"
316
343
  );
344
+ expect(link).not.toBeNull();
345
+ expect(link.textContent).toBe("Omega Library - omega");
346
+ expect(link.href).toContain("/admin/web/config/libraries/edit/uuid-omega");
317
347
  });
318
348
 
319
349
  // ── Per-service isolation ─────────────────────────────────────────────────
@@ -290,9 +290,9 @@ describe("EditableConfigList - library association disclosure", () => {
290
290
 
291
291
  const items = container.querySelectorAll(".associated-items li");
292
292
  expect(items).toHaveLength(3);
293
- expect(items[0].textContent).toBe("Alpha Library");
294
- expect(items[1].textContent).toBe("Beta Library");
295
- expect(items[2].textContent).toBe("Gamma Library");
293
+ expect(items[0].textContent).toBe("Alpha Library - alpha");
294
+ expect(items[1].textContent).toBe("Beta Library - beta");
295
+ expect(items[2].textContent).toBe("Gamma Library - gamma");
296
296
  });
297
297
 
298
298
  it("renders a library with a uuid as a link to its config page", () => {
@@ -305,7 +305,7 @@ describe("EditableConfigList - library association disclosure", () => {
305
305
  ".associated-items a"
306
306
  );
307
307
  expect(link).not.toBeNull();
308
- expect(link.textContent).toBe("Alpha Library");
308
+ expect(link.textContent).toBe("Alpha Library - alpha");
309
309
  expect(link.href).toContain("/admin/web/config/libraries/edit/uuid-alpha");
310
310
  });
311
311
 
@@ -317,7 +317,7 @@ describe("EditableConfigList - library association disclosure", () => {
317
317
 
318
318
  expect(container.querySelector(".associated-items a")).toBeNull();
319
319
  expect(container.querySelector(".associated-items li").textContent).toBe(
320
- "Delta Library"
320
+ "Delta Library - delta"
321
321
  );
322
322
  });
323
323
 
@@ -373,8 +373,8 @@ describe("EditableConfigList - library association disclosure", () => {
373
373
  const items = container.querySelectorAll(".associated-items li");
374
374
  expect(items).toHaveLength(2);
375
375
  // Sorted alphabetically by resolved display name.
376
- expect(items[0].textContent).toBe("Alpha Library");
377
- expect(items[1].textContent).toBe("Beta Library");
376
+ expect(items[0].textContent).toBe("Alpha Library - alpha");
377
+ expect(items[1].textContent).toBe("Beta Library - beta");
378
378
  });
379
379
 
380
380
  it("falls back to short_name when allLibraries is absent from the data", () => {
@@ -246,6 +246,12 @@ describe("IndividualAdminEditForm - rendered inputs and role changes", () => {
246
246
  expect(email.readOnly).toBe(true);
247
247
  });
248
248
 
249
+ it("labels each library row in the role table with the name and short name", () => {
250
+ renderForm();
251
+ expect(screen.getByText("NYPL - nypl")).toBeInTheDocument();
252
+ expect(screen.getByText("BPL - bpl")).toBeInTheDocument();
253
+ });
254
+
249
255
  it("requires the email and password fields", () => {
250
256
  const { container } = renderForm();
251
257
  expect(container.querySelectorAll(".required-field")).toHaveLength(2);
@@ -148,7 +148,7 @@ describe("IndividualAdmins - role association disclosure", () => {
148
148
 
149
149
  const items = container.querySelectorAll(".associated-items li");
150
150
  expect(items).toHaveLength(1);
151
- expect(items[0].textContent).toBe("Alpha Library - Manager");
151
+ expect(items[0].textContent).toBe("Alpha Library - alpha - Manager");
152
152
  });
153
153
 
154
154
  it("shows '<library> - Librarian' for a librarian role", () => {
@@ -162,7 +162,7 @@ describe("IndividualAdmins - role association disclosure", () => {
162
162
 
163
163
  const items = container.querySelectorAll(".associated-items li");
164
164
  expect(items).toHaveLength(1);
165
- expect(items[0].textContent).toBe("Beta Library - Librarian");
165
+ expect(items[0].textContent).toBe("Beta Library - beta - Librarian");
166
166
  });
167
167
 
168
168
  it("shows the highest role (Manager) when a library has both manager and librarian roles", () => {
@@ -179,7 +179,7 @@ describe("IndividualAdmins - role association disclosure", () => {
179
179
 
180
180
  const items = container.querySelectorAll(".associated-items li");
181
181
  expect(items).toHaveLength(1);
182
- expect(items[0].textContent).toBe("Alpha Library - Manager");
182
+ expect(items[0].textContent).toBe("Alpha Library - alpha - Manager");
183
183
  });
184
184
 
185
185
  // ── Sitewide roles ────────────────────────────────────────────────────────
@@ -238,8 +238,8 @@ describe("IndividualAdmins - role association disclosure", () => {
238
238
  const items = container.querySelectorAll(".associated-items li");
239
239
  const texts = Array.from(items).map((li) => li.textContent);
240
240
  expect(texts).toContain("All libraries - Manager");
241
- expect(texts).toContain("Alpha Library - Manager");
242
- expect(texts).toContain("Beta Library - Librarian");
241
+ expect(texts).toContain("Alpha Library - alpha - Manager");
242
+ expect(texts).toContain("Beta Library - beta - Librarian");
243
243
  });
244
244
 
245
245
  it("shows both 'All libraries' and per-library entries when librarian-all and library roles coexist", () => {
@@ -257,7 +257,7 @@ describe("IndividualAdmins - role association disclosure", () => {
257
257
  const items = container.querySelectorAll(".associated-items li");
258
258
  const texts = Array.from(items).map((li) => li.textContent);
259
259
  expect(texts).toContain("All libraries - Librarian");
260
- expect(texts).toContain("Gamma Library - Librarian");
260
+ expect(texts).toContain("Gamma Library - gamma - Librarian");
261
261
  });
262
262
 
263
263
  // ── Sorting ───────────────────────────────────────────────────────────────
@@ -276,9 +276,9 @@ describe("IndividualAdmins - role association disclosure", () => {
276
276
  fireEvent.click(container.querySelector(".association-toggle"));
277
277
 
278
278
  const items = container.querySelectorAll(".associated-items li");
279
- expect(items[0].textContent).toBe("Alpha Library - Manager");
280
- expect(items[1].textContent).toBe("Beta Library - Librarian");
281
- expect(items[2].textContent).toBe("Gamma Library - Manager");
279
+ expect(items[0].textContent).toBe("Alpha Library - alpha - Manager");
280
+ expect(items[1].textContent).toBe("Beta Library - beta - Librarian");
281
+ expect(items[2].textContent).toBe("Gamma Library - gamma - Manager");
282
282
  });
283
283
 
284
284
  // ── Links ─────────────────────────────────────────────────────────────────
@@ -296,7 +296,7 @@ describe("IndividualAdmins - role association disclosure", () => {
296
296
  ".associated-items a"
297
297
  );
298
298
  expect(link).not.toBeNull();
299
- expect(link.textContent).toBe("Alpha Library");
299
+ expect(link.textContent).toBe("Alpha Library - alpha");
300
300
  expect(link.href).toContain("/admin/web/config/libraries/edit/uuid-alpha");
301
301
  // The role suffix should not be part of the link.
302
302
  expect(link.nextSibling.textContent).toBe(" - Manager");
@@ -313,7 +313,22 @@ describe("IndividualAdmins - role association disclosure", () => {
313
313
 
314
314
  expect(container.querySelector(".associated-items a")).toBeNull();
315
315
  expect(container.querySelector(".associated-items li").textContent).toBe(
316
- "Delta Library - Librarian"
316
+ "Delta Library - delta - Librarian"
317
+ );
318
+ });
319
+
320
+ it("shows just the short name, unlinked, for a role whose library is missing from allLibraries", () => {
321
+ const { container } = renderAdmins([
322
+ {
323
+ email: "mgr@example.com",
324
+ roles: [{ role: "manager", library: "omega" }],
325
+ },
326
+ ]);
327
+ fireEvent.click(container.querySelector(".association-toggle"));
328
+
329
+ expect(container.querySelector(".associated-items a")).toBeNull();
330
+ expect(container.querySelector(".associated-items li").textContent).toBe(
331
+ "omega - Manager"
317
332
  );
318
333
  });
319
334
 
@@ -37,7 +37,7 @@ describe("Libraries - connected wiring", () => {
37
37
  // The library and its edit control appear once the on-mount fetch resolves
38
38
  // and mapStateToProps feeds the fetched data back in as props.
39
39
  expect(
40
- await screen.findByText("New York Public Library")
40
+ await screen.findByText("New York Public Library - nypl")
41
41
  ).toBeInTheDocument();
42
42
  const editLink = await screen.findByRole("link", { name: /edit/i });
43
43
  expect(editLink).toHaveAttribute(
@@ -45,4 +45,23 @@ describe("Libraries - connected wiring", () => {
45
45
  "/admin/web/config/libraries/edit/uuid-nypl"
46
46
  );
47
47
  });
48
+
49
+ it("falls back to the uuid as the label for a library with no name and no short name", async () => {
50
+ jest.spyOn(globalThis, "fetch").mockImplementation(
51
+ async () =>
52
+ new Response(
53
+ JSON.stringify({
54
+ libraries: [{ uuid: "uuid-anon", settings: {} }],
55
+ }),
56
+ { status: 200, headers: { "Content-Type": "application/json" } }
57
+ )
58
+ );
59
+
60
+ renderWithProviders(<Libraries csrfToken="token" />, {
61
+ reduxProviderProps: { store: buildStore() },
62
+ appConfigSettings: { roles: [{ role: "system" }] },
63
+ });
64
+
65
+ expect(await screen.findByText("uuid-anon")).toBeInTheDocument();
66
+ });
48
67
  });
@@ -417,6 +417,72 @@ describe("ServiceEditForm", () => {
417
417
  expect(libraries[0]).toHaveTextContent("New York Public Library");
418
418
  });
419
419
 
420
+ it("labels an associated library with its name and short name", () => {
421
+ const { container } = renderForm({ item: serviceData });
422
+ const editable = container.querySelectorAll(".with-edit-button");
423
+ expect(editable).toHaveLength(1);
424
+ expect(editable[0]).toHaveTextContent("New York Public Library - nypl");
425
+ });
426
+
427
+ it("links an associated library to its configuration page", () => {
428
+ const dataWithUuids = Object.assign({}, servicesData, {
429
+ allLibraries: [
430
+ Object.assign({}, allLibraries[0], { uuid: "nypl-uuid" }),
431
+ allLibraries[1],
432
+ ],
433
+ });
434
+ const { container } = renderForm({
435
+ data: dataWithUuids,
436
+ item: serviceData,
437
+ });
438
+ const link = container.querySelector(
439
+ ".with-edit-button a"
440
+ ) as HTMLAnchorElement;
441
+ expect(link).toBeInTheDocument();
442
+ expect(link).toHaveAttribute(
443
+ "href",
444
+ "/admin/web/config/libraries/edit/nypl-uuid"
445
+ );
446
+ expect(link).toHaveTextContent("New York Public Library - nypl");
447
+ });
448
+
449
+ it("opens the associated library link in a new tab, so unsaved edits survive a stray click", () => {
450
+ const dataWithUuids = Object.assign({}, servicesData, {
451
+ allLibraries: [
452
+ Object.assign({}, allLibraries[0], { uuid: "nypl-uuid" }),
453
+ allLibraries[1],
454
+ ],
455
+ });
456
+ const { container } = renderForm({
457
+ data: dataWithUuids,
458
+ item: serviceData,
459
+ });
460
+ const link = container.querySelector(
461
+ ".with-edit-button a"
462
+ ) as HTMLAnchorElement;
463
+ expect(link).toHaveAttribute("target", "_blank");
464
+ expect(link).toHaveAttribute("rel", "noopener noreferrer");
465
+ expect(link).toHaveAccessibleName(
466
+ "New York Public Library - nypl (opens in a new tab)"
467
+ );
468
+ // The visible new-tab cue for sighted users; hidden from screen readers,
469
+ // which get the aria-label wording instead.
470
+ const icon = link.querySelector("i.fa-external-link");
471
+ expect(icon).toBeInTheDocument();
472
+ expect(icon).toHaveAttribute("aria-hidden", "true");
473
+ });
474
+
475
+ it("falls back to the short name for a library missing from allLibraries", () => {
476
+ const serviceWithUnknownLibrary = Object.assign({}, serviceData, {
477
+ libraries: [{ short_name: "unknown" }],
478
+ });
479
+ const { container } = renderForm({ item: serviceWithUnknownLibrary });
480
+ const editable = container.querySelectorAll(".with-edit-button");
481
+ expect(editable).toHaveLength(1);
482
+ expect(editable[0]).toHaveTextContent("unknown");
483
+ expect(container.querySelector(".with-edit-button a")).toBeNull();
484
+ });
485
+
420
486
  it("renders removable and editable libraries", () => {
421
487
  const { container, unmount } = renderForm();
422
488
  expect(container.querySelectorAll(".with-remove-button")).toHaveLength(0);
@@ -22,8 +22,9 @@ const protocolsData = [
22
22
  },
23
23
  ];
24
24
  const allLibraries = [
25
- { short_name: "nypl", name: "New York Public Library" },
26
- { short_name: "bpl", name: "Brooklyn Public Library" },
25
+ { short_name: "nypl", name: "New York Public Library", uuid: "uuid-nypl" },
26
+ { short_name: "bpl", name: "Brooklyn Public Library", uuid: "uuid-bpl" },
27
+ // No uuid, so this one has no configuration page to link to.
27
28
  { short_name: "qpl", name: "Queens Public Library" },
28
29
  ];
29
30
  const libraryRegistrationsData = [
@@ -75,15 +76,34 @@ describe("ServiceWithRegistrationsEditForm", () => {
75
76
  screen.getByRole("heading", { name: "Register libraries" })
76
77
  ).toBeInTheDocument();
77
78
 
79
+ // Each library is labeled "<name> - <short name>" and, when its uuid is
80
+ // known, links to its configuration page in a new tab.
81
+ const nyplLink = screen.getByRole("link", {
82
+ name: "New York Public Library - nypl (opens in a new tab)",
83
+ });
84
+ expect(nyplLink).toHaveAttribute(
85
+ "href",
86
+ "/admin/web/config/libraries/edit/uuid-nypl"
87
+ );
88
+ expect(nyplLink).toHaveAttribute("target", "_blank");
89
+ expect(nyplLink).toHaveAttribute("rel", "noopener noreferrer");
90
+
78
91
  expect(
79
- screen.getByRole("link", { name: "New York Public Library" })
92
+ screen.getByRole("link", {
93
+ name: "Brooklyn Public Library - bpl (opens in a new tab)",
94
+ })
80
95
  ).toBeInTheDocument();
96
+
97
+ // Without a uuid there is no link, just the label. The same label also
98
+ // appears as an "Add Library" option, so scope to the registration list.
81
99
  expect(
82
- screen.getByRole("link", { name: "Brooklyn Public Library" })
83
- ).toBeInTheDocument();
100
+ screen
101
+ .getAllByText("Queens Public Library - qpl")
102
+ .some((element) => element.classList.contains("library-name"))
103
+ ).toBe(true);
84
104
  expect(
85
- screen.getByRole("link", { name: "Queens Public Library" })
86
- ).toBeInTheDocument();
105
+ screen.queryByRole("link", { name: /Queens Public Library/ })
106
+ ).toBeNull();
87
107
 
88
108
  // Statuses come from `data.libraryRegistrations`.
89
109
  expect(screen.getByText("Registered")).toHaveClass("bg-success");
@@ -2,6 +2,8 @@ import {
2
2
  findDefault,
3
3
  formatString,
4
4
  isEqual,
5
+ libraryConfigHref,
6
+ libraryLabel,
5
7
  } from "../../../src/utils/sharedFunctions";
6
8
 
7
9
  describe("findDefault", () => {
@@ -119,3 +121,33 @@ describe("isEqual", () => {
119
121
  expect(isEqual(["a", "a", "b"], ["b", "a", "a"])).toBe(true);
120
122
  });
121
123
  });
124
+
125
+ describe("libraryLabel", () => {
126
+ it("combines the name and the short name", () => {
127
+ expect(libraryLabel("New York Public Library", "nypl")).toBe(
128
+ "New York Public Library - nypl"
129
+ );
130
+ });
131
+ it("falls back to the short name when there is no name", () => {
132
+ expect(libraryLabel(undefined, "nypl")).toBe("nypl");
133
+ });
134
+ it("falls back to the name when there is no short name", () => {
135
+ expect(libraryLabel("New York Public Library", undefined)).toBe(
136
+ "New York Public Library"
137
+ );
138
+ });
139
+ it("returns a placeholder when neither is available", () => {
140
+ expect(libraryLabel(undefined, undefined)).toBe("(unnamed)");
141
+ });
142
+ });
143
+
144
+ describe("libraryConfigHref", () => {
145
+ it("builds the library configuration url from a uuid", () => {
146
+ expect(libraryConfigHref("uuid-nypl")).toBe(
147
+ "/admin/web/config/libraries/edit/uuid-nypl"
148
+ );
149
+ });
150
+ it("returns undefined without a uuid", () => {
151
+ expect(libraryConfigHref(undefined)).toBeUndefined();
152
+ });
153
+ });