Skip to content

Commit 147c244

Browse files
jonathangreendbernstein
authored andcommitted
Add collection import panel with documentation (PP-3862) (#208)
## Description Adds an import panel to `CollectionImportButton` for collections whose protocol supports import: - **Dynamic button label and style:** Button shows "Queue Full Re-import" when force is checked, making the heavier action visually distinct - **Context-aware success messages:** Regular imports note that new/updated items will appear; force re-imports note that all items will be re-processed and it may take longer - **Compact description** below the controls using the standard `description` class, with a "More details" toggle for full documentation ## Motivation and Context PP-3862 — Add description to collection Import function in Collection Manager. <img width="756" height="232" alt="Screenshot 2026-03-27 at 10 52 33 AM" src="https://github.com/user-attachments/assets/8bad8ff1-cc8f-4d73-9ab3-c7e1ac98ca9b" /> <img width="759" height="424" alt="Screenshot 2026-03-27 at 10 52 40 AM" src="https://github.com/user-attachments/assets/c017b117-6be5-4733-a6e1-e17caea22708" /> <img width="756" height="421" alt="Screenshot 2026-03-27 at 10 52 49 AM" src="https://github.com/user-attachments/assets/24a261ef-114e-4952-920a-e9bceb3529d9" /> ## How Has This Been Tested? - All 19 Jest tests pass (`npm run test-jest-file tests/jest/components/CollectionImportButton.test.tsx`) - ESLint and Prettier pass via pre-commit hooks ## Checklist: - [x] I have updated the documentation accordingly. - [x] All new and existing tests passed.
1 parent bc8697f commit 147c244

3 files changed

Lines changed: 253 additions & 13 deletions

File tree

src/components/CollectionImportButton.tsx

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import * as React from "react";
22
import { Panel } from "library-simplified-reusable-components";
33
import { CollectionData, ProtocolData } from "../interfaces";
44

5+
const IMPORT_DEFAULT_LABEL_TEXT = "Queue Import";
6+
const IMPORT_FORCED_FULL_LABEL_TEXT = "Force full re-import";
7+
58
export interface CollectionImportButtonProps {
69
collection: CollectionData;
710
protocols: ProtocolData[];
@@ -48,7 +51,11 @@ const CollectionImportButton: React.FC<CollectionImportButtonProps> = ({
4851
try {
4952
await importCollection(collection.id, force);
5053
setImporting(false);
51-
setFeedback("Import task queued.");
54+
setFeedback(
55+
force
56+
? "Full re-import task queued. All items will be re-processed — this may take longer than a regular import. Changes will appear in the catalog once processing completes."
57+
: "Import task queued. New and updated items will appear in the catalog once processing completes."
58+
);
5259
setSuccess(true);
5360
} catch (e) {
5461
const message =
@@ -67,16 +74,21 @@ const CollectionImportButton: React.FC<CollectionImportButtonProps> = ({
6774

6875
const feedbackClass = success ? "alert alert-success" : "alert alert-danger";
6976

77+
const buttonLabel = getButtonLabel(force, importing);
78+
79+
const buttonClass = force
80+
? "btn btn-default collection-import-button force"
81+
: "btn btn-default collection-import-button";
82+
7083
const panelContent = (
7184
<div className="collection-import">
72-
{feedback && <div className={feedbackClass}>{feedback}</div>}
7385
<div className="collection-import-controls">
7486
<button
75-
className="btn btn-default"
87+
className={buttonClass}
7688
disabled={disabled || importing}
7789
onClick={handleImport}
7890
>
79-
{importing ? "Queuing..." : "Queue Import"}
91+
{buttonLabel}
8092
</button>
8193
<label>
8294
<input
@@ -85,9 +97,38 @@ const CollectionImportButton: React.FC<CollectionImportButtonProps> = ({
8597
onChange={(e) => setForce(e.target.checked)}
8698
disabled={disabled || importing}
8799
/>{" "}
88-
Force full re-import
100+
{IMPORT_FORCED_FULL_LABEL_TEXT}
89101
</label>
90102
</div>
103+
{feedback && <div className={feedbackClass}>{feedback}</div>}
104+
<p className="description">
105+
{IMPORT_DEFAULT_LABEL_TEXT} picks up new and changed items. Check{" "}
106+
<strong>{IMPORT_FORCED_FULL_LABEL_TEXT}</strong> to re-process
107+
everything.
108+
</p>
109+
<details className="collection-import-details" key={collection?.id}>
110+
<summary>More details</summary>
111+
<dl className="collection-import-docs">
112+
<dt>{IMPORT_DEFAULT_LABEL_TEXT}</dt>
113+
<dd>
114+
Schedules a background import job that checks for new or updated
115+
items from the collection source and adds them to the catalog. Only
116+
items that have changed since the last import are processed. Use
117+
this when new titles have been added to a collection but do not yet
118+
appear in the catalog, or when you want to pick up recent changes
119+
from the source.
120+
</dd>
121+
<dt>{IMPORT_FORCED_FULL_LABEL_TEXT}</dt>
122+
<dd>
123+
When checked, the import job re-processes every item in the
124+
collection, regardless of whether it appears to have changed since
125+
the last import. Use this to correct metadata that is out of date,
126+
or to resolve issues caused by a previously incomplete import. A
127+
forced re-import will take longer than a regular import because it
128+
re-processes all items.
129+
</dd>
130+
</dl>
131+
</details>
91132
</div>
92133
);
93134

@@ -96,4 +137,11 @@ const CollectionImportButton: React.FC<CollectionImportButtonProps> = ({
96137
);
97138
};
98139

140+
function getButtonLabel(force: boolean, importing: boolean): string {
141+
if (force) {
142+
return importing ? "Queuing Full Re-import..." : "Queue Full Re-import";
143+
}
144+
return importing ? "Queuing..." : "Queue Import";
145+
}
146+
99147
export default CollectionImportButton;

src/stylesheets/collection.scss

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,66 @@
55
}
66

77
i {
8-
color: #AAA;
8+
color: $medium-dark-gray;
99
cursor: pointer;
1010
}
1111
}
1212

1313
.collection-import {
14+
.collection-import-button.force {
15+
background: darken($yellow, 8%);
16+
border-color: darken($yellow, 20%);
17+
color: $dark-gray;
18+
19+
&:hover,
20+
&:focus-visible {
21+
background: $yellow;
22+
border-color: darken($yellow, 25%);
23+
color: $dark-gray;
24+
}
25+
}
26+
27+
details.collection-import-details {
28+
margin-bottom: 1em;
29+
30+
summary {
31+
list-style: none;
32+
color: $dark-gray;
33+
cursor: pointer;
34+
font-size: 0.8rem;
35+
text-decoration: underline;
36+
37+
&::-webkit-details-marker {
38+
display: none;
39+
}
40+
41+
&:hover,
42+
&:focus-visible {
43+
color: $blue-dark;
44+
text-decoration: underline;
45+
}
46+
}
47+
}
48+
49+
.collection-import-docs {
50+
font-size: 0.8rem;
51+
margin-bottom: 1em;
52+
53+
dt {
54+
font-weight: bold;
55+
margin-top: 0.75em;
56+
57+
&:first-child {
58+
margin-top: 0;
59+
}
60+
}
61+
62+
dd {
63+
margin-left: 0;
64+
color: $dark-gray;
65+
}
66+
}
67+
1468
.collection-import-controls {
1569
display: flex;
1670
align-items: center;

tests/jest/components/CollectionImportButton.test.tsx

Lines changed: 145 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,56 @@ describe("CollectionImportButton", () => {
7979
screen.getByRole("button", { name: "Queue Import" })
8080
).toBeInTheDocument();
8181
expect(screen.getByRole("checkbox")).toBeInTheDocument();
82-
expect(screen.getByText("Force full re-import")).toBeInTheDocument();
82+
expect(screen.getByLabelText("Force full re-import")).toBeInTheDocument();
83+
});
84+
85+
it("shows compact summary by default; detailed docs are hidden", async () => {
86+
const user = userEvent.setup();
87+
renderButton();
88+
await expandPanel(user);
89+
expect(
90+
screen.getByText(/queue import picks up new and changed items/i)
91+
).toBeInTheDocument();
92+
expect(
93+
screen.getByText(/schedules a background import job/i)
94+
).not.toBeVisible();
95+
expect(
96+
screen.getByText(/the import job re-processes every item/i)
97+
).not.toBeVisible();
98+
});
99+
100+
it("clicking 'More details' reveals the detailed docs", async () => {
101+
const user = userEvent.setup();
102+
renderButton();
103+
await expandPanel(user);
104+
105+
const details = screen.getByText("More details").closest("details");
106+
expect(details).not.toHaveAttribute("open");
107+
108+
await user.click(screen.getByText("More details"));
109+
110+
expect(details).toHaveAttribute("open");
111+
expect(
112+
screen.getByText(/schedules a background import job/i)
113+
).toBeVisible();
114+
expect(
115+
screen.getByText(/the import job re-processes every item/i)
116+
).toBeVisible();
117+
});
118+
119+
it("clicking 'More details' again hides the detailed docs", async () => {
120+
const user = userEvent.setup();
121+
renderButton();
122+
await expandPanel(user);
123+
124+
await user.click(screen.getByText("More details"));
125+
expect(
126+
screen.getByText(/schedules a background import job/i)
127+
).toBeVisible();
128+
129+
await user.click(screen.getByText("More details"));
130+
const details = screen.getByText("More details").closest("details");
131+
expect(details).not.toHaveAttribute("open");
83132
});
84133

85134
it("checkbox toggles force state", async () => {
@@ -94,6 +143,41 @@ describe("CollectionImportButton", () => {
94143
expect(checkbox).not.toBeChecked();
95144
});
96145

146+
it("button text changes to 'Queue Full Re-import' when force is checked", async () => {
147+
const user = userEvent.setup();
148+
renderButton();
149+
await expandPanel(user);
150+
151+
expect(
152+
screen.getByRole("button", { name: "Queue Import" })
153+
).toBeInTheDocument();
154+
155+
await user.click(screen.getByRole("checkbox"));
156+
157+
expect(
158+
screen.getByRole("button", { name: "Queue Full Re-import" })
159+
).toBeInTheDocument();
160+
expect(
161+
screen.queryByRole("button", { name: "Queue Import" })
162+
).not.toBeInTheDocument();
163+
});
164+
165+
it("button uses force class when force is checked", async () => {
166+
const user = userEvent.setup();
167+
renderButton();
168+
await expandPanel(user);
169+
170+
const button = screen.getByRole("button", { name: "Queue Import" });
171+
expect(button).not.toHaveClass("force");
172+
173+
await user.click(screen.getByRole("checkbox"));
174+
175+
const forceButton = screen.getByRole("button", {
176+
name: "Queue Full Re-import",
177+
});
178+
expect(forceButton).toHaveClass("force");
179+
});
180+
97181
it("button triggers import with correct args (force=false)", async () => {
98182
const user = userEvent.setup();
99183
const { importCollection } = renderButton();
@@ -109,18 +193,39 @@ describe("CollectionImportButton", () => {
109193
await expandPanel(user);
110194
const checkbox = screen.getByRole("checkbox");
111195
await user.click(checkbox);
112-
const button = screen.getByRole("button", { name: "Queue Import" });
196+
const button = screen.getByRole("button", {
197+
name: "Queue Full Re-import",
198+
});
113199
await user.click(button);
114200
expect(importCollection).toHaveBeenCalledWith(42, true);
115201
});
116202

117-
it("shows success feedback with alert-success styling after import", async () => {
203+
it("shows success feedback for regular import", async () => {
118204
const user = userEvent.setup();
119205
renderButton();
120206
await expandPanel(user);
121207
await user.click(screen.getByRole("button", { name: "Queue Import" }));
122208
await waitFor(() => {
123-
const feedback = screen.getByText("Import task queued.");
209+
const feedback = screen.getByText(
210+
/import task queued\. new and updated items will appear/i
211+
);
212+
expect(feedback).toBeInTheDocument();
213+
expect(feedback).toHaveClass("alert", "alert-success");
214+
});
215+
});
216+
217+
it("shows success feedback for force re-import", async () => {
218+
const user = userEvent.setup();
219+
renderButton();
220+
await expandPanel(user);
221+
await user.click(screen.getByRole("checkbox"));
222+
await user.click(
223+
screen.getByRole("button", { name: "Queue Full Re-import" })
224+
);
225+
await waitFor(() => {
226+
const feedback = screen.getByText(
227+
/full re-import task queued\. all items will be re-processed/i
228+
);
124229
expect(feedback).toBeInTheDocument();
125230
expect(feedback).toHaveClass("alert", "alert-success");
126231
});
@@ -150,9 +255,13 @@ describe("CollectionImportButton", () => {
150255
await user.click(checkbox);
151256
expect(checkbox).toBeChecked();
152257

153-
await user.click(screen.getByRole("button", { name: "Queue Import" }));
258+
await user.click(
259+
screen.getByRole("button", { name: "Queue Full Re-import" })
260+
);
154261
await waitFor(() => {
155-
expect(screen.getByText("Import task queued.")).toBeInTheDocument();
262+
expect(
263+
screen.getByText(/full re-import task queued/i)
264+
).toBeInTheDocument();
156265
});
157266

158267
const nextCollection: CollectionData = {
@@ -171,7 +280,9 @@ describe("CollectionImportButton", () => {
171280

172281
await waitFor(() => {
173282
expect(screen.getByRole("checkbox")).not.toBeChecked();
174-
expect(screen.queryByText("Import task queued.")).not.toBeInTheDocument();
283+
expect(
284+
screen.queryByText(/full re-import task queued/i)
285+
).not.toBeInTheDocument();
175286
});
176287
});
177288

@@ -204,4 +315,31 @@ describe("CollectionImportButton", () => {
204315
).toBeEnabled();
205316
});
206317
});
318+
319+
it("shows 'Queuing Full Re-import...' while importing with force", async () => {
320+
const user = userEvent.setup();
321+
let resolveImport: () => void;
322+
const pendingImport = new Promise<void>((resolve) => {
323+
resolveImport = resolve;
324+
});
325+
const mockImport = jest.fn().mockReturnValue(pendingImport);
326+
renderButton({ importCollection: mockImport });
327+
await expandPanel(user);
328+
329+
await user.click(screen.getByRole("checkbox"));
330+
await user.click(
331+
screen.getByRole("button", { name: "Queue Full Re-import" })
332+
);
333+
334+
expect(
335+
screen.getByRole("button", { name: "Queuing Full Re-import..." })
336+
).toBeDisabled();
337+
338+
resolveImport();
339+
await waitFor(() => {
340+
expect(
341+
screen.getByRole("button", { name: "Queue Full Re-import" })
342+
).toBeEnabled();
343+
});
344+
});
207345
});

0 commit comments

Comments
 (0)