diff --git a/src/components/CollectionImportButton.tsx b/src/components/CollectionImportButton.tsx index 0b1e35a4a..063124d72 100644 --- a/src/components/CollectionImportButton.tsx +++ b/src/components/CollectionImportButton.tsx @@ -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[]; @@ -48,7 +51,11 @@ const CollectionImportButton: React.FC = ({ 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 = @@ -67,16 +74,21 @@ const CollectionImportButton: React.FC = ({ 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 = (
- {feedback &&
{feedback}
}
+ {feedback &&
{feedback}
} +

+ {IMPORT_DEFAULT_LABEL_TEXT} picks up new and changed items. Check{" "} + {IMPORT_FORCED_FULL_LABEL_TEXT} to re-process + everything. +

+
+ More details +
+
{IMPORT_DEFAULT_LABEL_TEXT}
+
+ 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. +
+
{IMPORT_FORCED_FULL_LABEL_TEXT}
+
+ 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. +
+
+
); @@ -96,4 +137,11 @@ const CollectionImportButton: React.FC = ({ ); }; +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; diff --git a/src/stylesheets/collection.scss b/src/stylesheets/collection.scss index acf40db02..5199adae1 100644 --- a/src/stylesheets/collection.scss +++ b/src/stylesheets/collection.scss @@ -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; diff --git a/tests/jest/components/CollectionImportButton.test.tsx b/tests/jest/components/CollectionImportButton.test.tsx index f08be022a..3321c6081 100644 --- a/tests/jest/components/CollectionImportButton.test.tsx +++ b/tests/jest/components/CollectionImportButton.test.tsx @@ -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 () => { @@ -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(); @@ -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"); }); @@ -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 = { @@ -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(); }); }); @@ -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((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(); + }); + }); });