Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export default class ActionCreator extends BaseActionCreator {
static readonly COLLECTIONS = "COLLECTIONS";
static readonly EDIT_COLLECTION = "EDIT_COLLECTION";
static readonly DELETE_COLLECTION = "DELETE_COLLECTION";
static readonly IMPORT_COLLECTION = "IMPORT_COLLECTION";
static readonly INDIVIDUAL_ADMINS = "INDIVIDUAL_ADMINS";
static readonly EDIT_INDIVIDUAL_ADMIN = "EDIT_INDIVIDUAL_ADMIN";
static readonly DELETE_INDIVIDUAL_ADMIN = "DELETE_INDIVIDUAL_ADMIN";
Expand Down Expand Up @@ -451,6 +452,13 @@ export default class ActionCreator extends BaseActionCreator {
).bind(this);
}

importCollection(collectionId: string | number, force: boolean) {
const url = "/admin/collection/" + collectionId + "/import";
const data = new FormData();
data.append("force", String(force));
return this.postForm(ActionCreator.IMPORT_COLLECTION, url, data).bind(this);
}

fetchIndividualAdmins() {
const url = "/admin/individual_admins";
return this.fetchJSON<IndividualAdminsData>(
Expand Down Expand Up @@ -1002,14 +1010,15 @@ export default class ActionCreator extends BaseActionCreator {
}

fetchQuicksightEmbedUrl(dashboardId: string, ld: LibrariesData) {

/* Too many libraries will blow up the 8K cloudfront/nginx max url size limit.
By not sending any uuids, the client will assemble a list of libraries based
on the user's permissions.
*/
let library_uuids: string = "";
if (ld.libraries.length < 100) {
library_uuids = `?libraryUuids=${ld.libraries.map((l) => l.uuid).join(",")}`;
library_uuids = `?libraryUuids=${ld.libraries
.map((l) => l.uuid)
.join(",")}`;
}
const url = `/admin/quicksight_embed/${dashboardId}${library_uuids}`;
return this.fetchJSON<QuickSightEmbeddedURLData>(
Expand Down
128 changes: 128 additions & 0 deletions src/components/CollectionImportButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import * as React from "react";
import { Panel } from "library-simplified-reusable-components";
import { CollectionData, ProtocolData } from "../interfaces";

export interface CollectionImportButtonProps {
collection: CollectionData;
protocols: ProtocolData[];
importCollection: (
collectionId: string | number,
force: boolean
) => Promise<void>;
disabled?: boolean;
}

interface CollectionImportButtonState {
force: boolean;
importing: boolean;
feedback: string | null;
success: boolean;
}

/**
* Renders an import button and "force" checkbox for collections
* whose protocol supports import. Renders nothing otherwise.
*/
export default class CollectionImportButton extends React.Component<
CollectionImportButtonProps,
CollectionImportButtonState
> {
constructor(props: CollectionImportButtonProps) {
super(props);
this.state = {
force: false,
importing: false,
feedback: null,
success: false,
};
this.handleImport = this.handleImport.bind(this);
this.handleForceChange = this.handleForceChange.bind(this);
}

componentDidUpdate(prevProps: CollectionImportButtonProps): void {
if (prevProps.collection?.id !== this.props.collection?.id) {
this.setState({
force: false,
importing: false,
feedback: null,
success: false,
});
}
}

supportsImport(): boolean {
const { collection, protocols } = this.props;
if (!collection?.id || !collection?.protocol) {
return false;
}
const protocol = protocols.find((p) => p.name === collection.protocol);
return !!protocol?.supports_import;
}

async handleImport(): Promise<void> {
const { collection, importCollection } = this.props;
this.setState({ importing: true, feedback: null });
try {
await importCollection(collection.id, this.state.force);
this.setState({
importing: false,
feedback: "Import task queued.",
success: true,
});
} catch (e) {
const message =
e && typeof e === "object" && "response" in e
? String((e as { response: string }).response)
: "Failed to queue import task.";
this.setState({ importing: false, feedback: message, success: false });
}
}

handleForceChange(e: React.ChangeEvent<HTMLInputElement>): void {
this.setState({ force: e.target.checked });
}

render(): JSX.Element | null {
if (!this.supportsImport()) {
return null;
}

const { disabled } = this.props;
const { force, importing, feedback, success } = this.state;
const feedbackClass = success
? "alert alert-success"
: "alert alert-danger";

const panelContent = (
<div className="collection-import">
{feedback && <div className={feedbackClass}>{feedback}</div>}
<div style={{ display: "flex", alignItems: "center", gap: "1em" }}>
<button
className="btn btn-default"
disabled={disabled || importing}
onClick={this.handleImport}
>
{importing ? "Queuing..." : "Queue Import"}
</button>
<label style={{ margin: 0 }}>
<input
type="checkbox"
checked={force}
onChange={this.handleForceChange}
disabled={disabled || importing}
/>{" "}
Force full re-import
</label>
</div>
</div>
);

return (
<Panel
id="collection-import"
headerText="Import"
content={panelContent}
/>
);
}
}
39 changes: 39 additions & 0 deletions src/components/Collections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
LibraryWithSettingsData,
LibraryRegistrationsData,
} from "../interfaces";
import CollectionImportButton from "./CollectionImportButton";
import ServiceWithRegistrationsEditForm from "./ServiceWithRegistrationsEditForm";
import TrashIcon from "./icons/TrashIcon";

Expand All @@ -27,6 +28,10 @@ export interface CollectionsDispatchProps
extends EditableConfigListDispatchProps<CollectionsData> {
registerLibrary: (data: FormData) => Promise<void>;
fetchLibraryRegistrations?: () => Promise<LibraryRegistrationsData>;
importCollection: (
collectionId: string | number,
force: boolean
) => Promise<void>;
}

export interface CollectionsProps
Expand All @@ -37,6 +42,19 @@ export interface CollectionsProps
export class CollectionEditForm extends ServiceWithRegistrationsEditForm<
CollectionsData
> {
context: {
registerLibrary: (library, registration_stage) => void;
importCollection: (
collectionId: string | number,
force: boolean
) => Promise<void>;
};

static contextTypes = {
...ServiceWithRegistrationsEditForm.contextTypes,
importCollection: PropTypes.func,
};

/**
* Override to display a confirmation message before removing a library
* association. We display the confirmation and return `true` if the
Expand All @@ -51,6 +69,21 @@ export class CollectionEditForm extends ServiceWithRegistrationsEditForm<
"remove all loans and holds for its patrons. Do you wish to continue?";
return window.confirm(confirmationMessage);
}

renderAdditionalContent(): React.ReactNode[] {
if (!this.props.item?.id) {
return [];
}
return [
<CollectionImportButton
key="import"
collection={this.props.item as CollectionData}
protocols={this.props.data?.protocols || []}
importCollection={this.context.importCollection}
disabled={this.props.disabled}
/>,
];
}
}

/**
Expand Down Expand Up @@ -78,6 +111,7 @@ export class Collections extends GenericEditableConfigList<

static childContextTypes: React.ValidationMap<any> = {
registerLibrary: PropTypes.func,
importCollection: PropTypes.func,
};

getChildContext() {
Expand All @@ -94,6 +128,9 @@ export class Collections extends GenericEditableConfigList<
});
}
},
importCollection: (collectionId: string | number, force: boolean) => {
return this.props.importCollection(collectionId, force);
},
};
}

Expand Down Expand Up @@ -174,6 +211,8 @@ function mapDispatchToProps(dispatch, ownProps) {
editItem: (data: FormData) => dispatch(actions.editCollection(data)),
deleteItem: (identifier: string | number) =>
dispatch(actions.deleteCollection(identifier)),
importCollection: (collectionId: string | number, force: boolean) =>
dispatch(actions.importCollection(collectionId, force)),
};
}

Expand Down
7 changes: 7 additions & 0 deletions src/components/ServiceEditForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,18 @@ export default class ServiceEditForm<
optionalFieldsPanel,
extraPanel,
librariesForm,
...this.renderAdditionalContent(),
]}
/>
);
}

/** Hook for subclasses to inject additional panels into the form content,
* rendered after the libraries panel but before the submit button. */
renderAdditionalContent(): React.ReactNode[] {
return [];
}

renderRequiredFields(
requiredFields,
protocol: ProtocolData,
Expand Down
6 changes: 6 additions & 0 deletions src/components/__tests__/Collections-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ describe("Collections", () => {
identifier="2"
registerLibrary={registerLibrary}
fetchLibraryRegistrations={fetchLibraryRegistrations}
importCollection={stub().returns(
new Promise<void>((resolve) => resolve())
)}
/>,
{ context: { admin: systemAdmin } }
);
Expand Down Expand Up @@ -92,6 +95,9 @@ describe("Collections", () => {
data={{ collections, protocols: [] }}
registerLibrary={registerLibrary}
fetchLibraryRegistrations={fetchLibraryRegistrations}
importCollection={stub().returns(
new Promise<void>((resolve) => resolve())
)}
/>,
{ context: { admin: systemAdmin } }
);
Expand Down
1 change: 1 addition & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ export interface ProtocolData {
sitewide?: boolean;
supports_registration?: boolean;
supports_staging?: boolean;
supports_import?: boolean;
settings: SettingData[];
child_settings?: SettingData[];
library_settings?: SettingData[];
Expand Down
Loading