Skip to content
58 changes: 53 additions & 5 deletions src/components/CollectionImportButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import * as React from "react";
import { Panel } from "library-simplified-reusable-components";
import { CollectionData, ProtocolData } from "../interfaces";

const IMPORT_DEFAULT_LABEL_TEXT = "Queue Import";
const IMPORT_FORCED_FULL_LABEL_TEXT = "Force full re-import";

export interface CollectionImportButtonProps {
collection: CollectionData;
protocols: ProtocolData[];
Expand Down Expand Up @@ -48,7 +51,11 @@ const CollectionImportButton: React.FC<CollectionImportButtonProps> = ({
try {
await importCollection(collection.id, force);
setImporting(false);
setFeedback("Import task queued.");
setFeedback(
force
? "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."
: "Import task queued. New and updated items will appear in the catalog once processing completes."
);
setSuccess(true);
} catch (e) {
const message =
Expand All @@ -67,16 +74,21 @@ const CollectionImportButton: React.FC<CollectionImportButtonProps> = ({

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

const buttonLabel = getButtonLabel(force, importing);

const buttonClass = force
? "btn btn-default collection-import-button force"
: "btn btn-default collection-import-button";

const panelContent = (
<div className="collection-import">
{feedback && <div className={feedbackClass}>{feedback}</div>}
<div className="collection-import-controls">
<button
className="btn btn-default"
className={buttonClass}
disabled={disabled || importing}
onClick={handleImport}
>
{importing ? "Queuing..." : "Queue Import"}
{buttonLabel}
</button>
<label>
<input
Expand All @@ -85,9 +97,38 @@ const CollectionImportButton: React.FC<CollectionImportButtonProps> = ({
onChange={(e) => setForce(e.target.checked)}
disabled={disabled || importing}
/>{" "}
Force full re-import
{IMPORT_FORCED_FULL_LABEL_TEXT}
</label>
</div>
{feedback && <div className={feedbackClass}>{feedback}</div>}
<p className="description">
{IMPORT_DEFAULT_LABEL_TEXT} picks up new and changed items. Check{" "}
<strong>{IMPORT_FORCED_FULL_LABEL_TEXT}</strong> to re-process
everything.
</p>
<details className="collection-import-details" key={collection?.id}>
<summary>More details</summary>
<dl className="collection-import-docs">
<dt>{IMPORT_DEFAULT_LABEL_TEXT}</dt>
<dd>
Schedules a background import job that checks for new or updated
items from the collection source and adds them to the catalog. Only
items that have changed since the last import are processed. Use
this when new titles have been added to a collection but do not yet
appear in the catalog, or when you want to pick up recent changes
from the source.
</dd>
<dt>{IMPORT_FORCED_FULL_LABEL_TEXT}</dt>
<dd>
When checked, the import job re-processes every item in the
collection, regardless of whether it appears to have changed since
the last import. Use this to correct metadata that is out of date,
or to resolve issues caused by a previously incomplete import. A
forced re-import will take longer than a regular import because it
re-processes all items.
</dd>
</dl>
</details>
</div>
);

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

function getButtonLabel(force: boolean, importing: boolean): string {
if (force) {
return importing ? "Queuing Full Re-import..." : "Queue Full Re-import";
}
return importing ? "Queuing..." : "Queue Import";
}

export default CollectionImportButton;
56 changes: 55 additions & 1 deletion src/stylesheets/collection.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,66 @@
}

i {
color: #AAA;
color: $medium-dark-gray;
cursor: pointer;
}
}

.collection-import {
.collection-import-button.force {
background: darken($yellow, 8%);
border-color: darken($yellow, 20%);
color: $dark-gray;

&:hover,
&:focus-visible {
background: $yellow;
border-color: darken($yellow, 25%);
color: $dark-gray;
}
}

details.collection-import-details {
margin-bottom: 1em;

summary {
list-style: none;
color: $dark-gray;
cursor: pointer;
font-size: 0.8rem;
text-decoration: underline;

&::-webkit-details-marker {
display: none;
}

&:hover,
&:focus-visible {
color: $blue-dark;
text-decoration: underline;
}
}
}

.collection-import-docs {
font-size: 0.8rem;
margin-bottom: 1em;

dt {
font-weight: bold;
margin-top: 0.75em;

&:first-child {
margin-top: 0;
}
}

dd {
margin-left: 0;
color: $dark-gray;
}
}

.collection-import-controls {
display: flex;
align-items: center;
Expand Down
152 changes: 145 additions & 7 deletions tests/jest/components/CollectionImportButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,56 @@ describe("CollectionImportButton", () => {
screen.getByRole("button", { name: "Queue Import" })
).toBeInTheDocument();
expect(screen.getByRole("checkbox")).toBeInTheDocument();
expect(screen.getByText("Force full re-import")).toBeInTheDocument();
expect(screen.getByLabelText("Force full re-import")).toBeInTheDocument();
});

it("shows compact summary by default; detailed docs are hidden", async () => {
const user = userEvent.setup();
renderButton();
await expandPanel(user);
expect(
screen.getByText(/queue import picks up new and changed items/i)
).toBeInTheDocument();
expect(
screen.getByText(/schedules a background import job/i)
).not.toBeVisible();
expect(
screen.getByText(/the import job re-processes every item/i)
).not.toBeVisible();
});

it("clicking 'More details' reveals the detailed docs", async () => {
const user = userEvent.setup();
renderButton();
await expandPanel(user);

const details = screen.getByText("More details").closest("details");
expect(details).not.toHaveAttribute("open");

await user.click(screen.getByText("More details"));

expect(details).toHaveAttribute("open");
expect(
screen.getByText(/schedules a background import job/i)
).toBeVisible();
expect(
screen.getByText(/the import job re-processes every item/i)
).toBeVisible();
});

it("clicking 'More details' again hides the detailed docs", async () => {
const user = userEvent.setup();
renderButton();
await expandPanel(user);

await user.click(screen.getByText("More details"));
expect(
screen.getByText(/schedules a background import job/i)
).toBeVisible();

await user.click(screen.getByText("More details"));
const details = screen.getByText("More details").closest("details");
expect(details).not.toHaveAttribute("open");
});

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

it("button text changes to 'Queue Full Re-import' when force is checked", async () => {
const user = userEvent.setup();
renderButton();
await expandPanel(user);

expect(
screen.getByRole("button", { name: "Queue Import" })
).toBeInTheDocument();

await user.click(screen.getByRole("checkbox"));

expect(
screen.getByRole("button", { name: "Queue Full Re-import" })
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Queue Import" })
).not.toBeInTheDocument();
});

it("button uses force class when force is checked", async () => {
const user = userEvent.setup();
renderButton();
await expandPanel(user);

const button = screen.getByRole("button", { name: "Queue Import" });
expect(button).not.toHaveClass("force");

await user.click(screen.getByRole("checkbox"));

const forceButton = screen.getByRole("button", {
name: "Queue Full Re-import",
});
expect(forceButton).toHaveClass("force");
});

it("button triggers import with correct args (force=false)", async () => {
const user = userEvent.setup();
const { importCollection } = renderButton();
Expand All @@ -109,18 +193,39 @@ describe("CollectionImportButton", () => {
await expandPanel(user);
const checkbox = screen.getByRole("checkbox");
await user.click(checkbox);
const button = screen.getByRole("button", { name: "Queue Import" });
const button = screen.getByRole("button", {
name: "Queue Full Re-import",
});
await user.click(button);
expect(importCollection).toHaveBeenCalledWith(42, true);
});

it("shows success feedback with alert-success styling after import", async () => {
it("shows success feedback for regular import", async () => {
const user = userEvent.setup();
renderButton();
await expandPanel(user);
await user.click(screen.getByRole("button", { name: "Queue Import" }));
await waitFor(() => {
const feedback = screen.getByText("Import task queued.");
const feedback = screen.getByText(
/import task queued\. new and updated items will appear/i
);
expect(feedback).toBeInTheDocument();
expect(feedback).toHaveClass("alert", "alert-success");
});
});

it("shows success feedback for force re-import", async () => {
const user = userEvent.setup();
renderButton();
await expandPanel(user);
await user.click(screen.getByRole("checkbox"));
await user.click(
screen.getByRole("button", { name: "Queue Full Re-import" })
);
await waitFor(() => {
const feedback = screen.getByText(
/full re-import task queued\. all items will be re-processed/i
);
expect(feedback).toBeInTheDocument();
expect(feedback).toHaveClass("alert", "alert-success");
});
Expand Down Expand Up @@ -150,9 +255,13 @@ describe("CollectionImportButton", () => {
await user.click(checkbox);
expect(checkbox).toBeChecked();

await user.click(screen.getByRole("button", { name: "Queue Import" }));
await user.click(
screen.getByRole("button", { name: "Queue Full Re-import" })
);
await waitFor(() => {
expect(screen.getByText("Import task queued.")).toBeInTheDocument();
expect(
screen.getByText(/full re-import task queued/i)
).toBeInTheDocument();
});

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

await waitFor(() => {
expect(screen.getByRole("checkbox")).not.toBeChecked();
expect(screen.queryByText("Import task queued.")).not.toBeInTheDocument();
expect(
screen.queryByText(/full re-import task queued/i)
).not.toBeInTheDocument();
});
});

Expand Down Expand Up @@ -204,4 +315,31 @@ describe("CollectionImportButton", () => {
).toBeEnabled();
});
});

it("shows 'Queuing Full Re-import...' while importing with force", async () => {
const user = userEvent.setup();
let resolveImport: () => void;
const pendingImport = new Promise<void>((resolve) => {
resolveImport = resolve;
});
const mockImport = jest.fn().mockReturnValue(pendingImport);
renderButton({ importCollection: mockImport });
await expandPanel(user);

await user.click(screen.getByRole("checkbox"));
await user.click(
screen.getByRole("button", { name: "Queue Full Re-import" })
);

expect(
screen.getByRole("button", { name: "Queuing Full Re-import..." })
).toBeDisabled();

resolveImport();
await waitFor(() => {
expect(
screen.getByRole("button", { name: "Queue Full Re-import" })
).toBeEnabled();
});
});
});
Loading