Skip to content

Commit

Permalink
Merge pull request #3509 from nulib/3235-transfer-file-sets
Browse files Browse the repository at this point in the history
Allow for transferring all file sets from one work to another
  • Loading branch information
bmquinn authored Aug 22, 2023
2 parents 27334b2 + a0722a1 commit c49e428
Show file tree
Hide file tree
Showing 14 changed files with 645 additions and 1 deletion.
25 changes: 25 additions & 0 deletions app/assets/js/components/Work/Tabs/Preservation/Preservation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import PropTypes from "prop-types";
import UISkeleton from "@js/components/UI/Skeleton";
import UITabsStickyHeader from "@js/components/UI/Tabs/StickyHeader";
import WorkTabsPreservationFileSetModal from "@js/components/Work/Tabs/Preservation/FileSetModal";
import WorkTabsPreservationTransferFileSetsModal from "@js/components/Work/Tabs/Preservation/TransferFileSetsModal";
import { useMutation, useQuery } from "@apollo/client";
import { sortFileSets, toastWrapper } from "@js/services/helpers";
import { formatDate } from "@js/services/helpers";
Expand All @@ -51,13 +52,19 @@ const WorkTabsPreservation = ({ work }) => {
fileset: {},
isVisible: false,
});
const [transferFilesetsModal, setTransferFilesetsModal] = React.useState({
fromWorkId: work.id,
isVisible: false,
});

const {
data: verifyFileSetsData,
error: verifyFileSetsError,
loading: verifyFileSetsLoading,
} = useQuery(VERIFY_FILE_SETS, { variables: { workId: work.id } });



/**
* Delete a Fileset
*/
Expand Down Expand Up @@ -180,6 +187,10 @@ const WorkTabsPreservation = ({ work }) => {
setTechnicalMetadata({ fileSet: { ...fileSet } });
};

const handleTransferFileSetsClick = () => {
setTransferFilesetsModal({ fromWorkId: work.id, isVisible: true });
};

return (
<div data-testid="preservation-tab">
<UITabsStickyHeader title="Preservation and Access">
Expand Down Expand Up @@ -304,9 +315,23 @@ const WorkTabsPreservation = ({ work }) => {
)}
</DialogContent>
</Dialog.Root>
<Button
as="span"
data-testid="button-transfer-file-sets"
isPrimary
onClick={handleTransferFileSetsClick}
>
Transfer File Sets to Existing Work
</Button>
</AuthDisplayAuthorized>
</div>

<WorkTabsPreservationTransferFileSetsModal
closeModal={() => setTransferFilesetsModal({ isVisible: false })}
isVisible={transferFilesetsModal.isVisible}
fromWorkId={work.id}
/>

<WorkTabsPreservationFileSetModal
closeModal={() => setIsAddFilesetModalVisible(false)}
isVisible={isAddFilesetModalVisible}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { useHistory } from "react-router-dom";
import { Button, Notification } from "@nulib/design-system";
import { TRANSFER_FILE_SETS } from "@js/components/Work/work.gql.js";
import { useMutation } from "@apollo/client";
import { toastWrapper } from "@js/services/helpers";
import { useForm, FormProvider } from "react-hook-form";
import Error from "@js/components/UI/Error";
import classNames from "classnames";
import UIFormField from "@js/components/UI/Form/Field.jsx";
import UIInput from "@js/components/UI/Form/Input";
import { css } from "@emotion/react";

const modalCss = css`
z-index: 100;
`;

function WorkTabsPreservationTransferFileSetsModal({
closeModal,
isVisible,
fromWorkId,
}) {
const defaultValues = {
fromWorkId: null,
};

const [formError, setFormError] = useState();
const [confirmationInput, setConfirmationInput] = useState("");
const [confirmationError, setConfirmationError] = useState();
const [isSubmitted, setIsSubmitted] = useState(false);
const [toWorkId, setToWorkId] = useState("");

const methods = useForm({
defaultValues: defaultValues,
shouldUnregister: false,
});

const history = useHistory();

const handleToWorkIdChange = (e) => {
setToWorkId(e.target.value);
};

const [transferFileSets, { loading, error, data }] = useMutation(
TRANSFER_FILE_SETS,
{
onCompleted({ transferFileSets }) {
toastWrapper(
"is-success",
`FileSets transferred successfully to work: ${transferFileSets.id}`
);
resetForm();
history.push(`/work/${transferFileSets.id}`);
},
onError(error) {
console.error(
"error in the transferFileSets GraphQL mutation :>> ",
error
);
console.error("error MESSAGE", error.message)
console.error("graphQL ERRORS", error.graphQLErrors)
setFormError(error);
},
}
);

const handleSubmit = (data) => {
setToWorkId(data.toWorkId);
setIsSubmitted(true);
if (confirmationInput !== "I understand") {
setConfirmationError({
confirmationText: "Confirmation text is required.",
});
return;
}
setConfirmationError(null);

transferFileSets({
variables: {
fromWorkId: fromWorkId,
toWorkId: data.toWorkId,
},
});
};

const handleCancel = () => {
resetForm();
closeModal();
};

const resetForm = () => {
methods.reset();
};

const handleConfirmationChange = (e) => {
const value = e.target.value;
setConfirmationInput(value);
setConfirmationError(null);
};

return (
<div
className={classNames("modal", {
"is-active": isVisible,
})}
css={modalCss}
>
<div className="modal-background"></div>
<FormProvider {...methods}>
<form
onSubmit={methods.handleSubmit(handleSubmit)}
data-testid="transfer-filesets-form"
>
<div className="modal-card">
<header className="modal-card-head">
<p className="modal-card-title">
Transfer FileSets between Works
</p>
<button
type="button"
className="delete"
aria-label="close"
onClick={handleCancel}
></button>
</header>

<section className="modal-card-body">
{error && <Error error={error} />}
<strong>From Work ID:</strong> {fromWorkId}
<UIFormField label="To Work ID:">
<UIInput
isReactHookForm
onChange={handleToWorkIdChange}
name="toWorkId"
label="To Work ID:"
data-testid="toWorkId"
/>
</UIFormField>
<Notification isCentered className="content">
<div className="block">
<strong>To execute this transfer, type "I understand"</strong>
</div>
<div>
<UIInput
isReactHookForm
onChange={handleConfirmationChange}
name="confirmationText"
label="Confirmation Text"
placeholder="I understand"
required
data-testid="confirmation-text"
/>
{isSubmitted && confirmationError && (
<p className="help is-danger">
{confirmationError.confirmationText}
</p>
)}
</div>
</Notification>
</section>

<footer className="modal-card-foot is-justify-content-flex-end">
<Button
isText
type="button"
onClick={handleCancel}
data-testid="cancel-button"
>
Cancel
</Button>
<Button
isPrimary
type="submit"
disabled={
loading ||
toWorkId === "" ||
confirmationInput !== "I understand"
}
data-testid="submit-button"
>
Transfer FileSets
</Button>
</footer>
</div>
</form>
</FormProvider>
</div>
);
}

WorkTabsPreservationTransferFileSetsModal.propTypes = {
closeModal: PropTypes.func,
isVisible: PropTypes.bool,
fromWorkId: PropTypes.string.isRequired,
};

export default WorkTabsPreservationTransferFileSetsModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from "react";
import TransferFileSetsModal from "./TransferFileSetsModal";
import {
renderWithRouterApollo,
withReactHookForm,
} from "@js/services/testing-helpers";
import { AuthProvider } from "@js/components/Auth/Auth";
import { mockWork } from "@js/components/Work/work.gql.mock.js";
import { screen } from "@testing-library/react";
import { getCurrentUserMock } from "@js/components/Auth/auth.gql.mock";

let isModalOpen = true;

const handleClose = () => {
isModalOpen = false;
};

describe("Transfer file sets to another work modal", () => {
beforeEach(() => {
const Wrapped = withReactHookForm(TransferFileSetsModal, {
closeModal: handleClose,
isVisible: isModalOpen,
fromWorkId: mockWork.id,
});
return renderWithRouterApollo(
<AuthProvider>
<Wrapped />
</AuthProvider>,
{
mocks: [
getCurrentUserMock,
],
}
);
});

it("renders fileset form", async () => {
expect(await screen.findByTestId("transfer-filesets-form"));
});
});
8 changes: 8 additions & 0 deletions app/assets/js/components/Work/work.gql.js
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,14 @@ export const UPDATE_ACCESS_FILE_ORDER = gql`
}
`;

export const TRANSFER_FILE_SETS = gql`
mutation TransferFileSets($fromWorkId: ID!, $toWorkId: ID!) {
transferFileSets(fromWorkId: $fromWorkId, toWorkId: $toWorkId) {
id
}
}
`;

export const WORK_ARCHIVER_ENDPOINT = gql`
query WorkArchiverEndpoint {
workArchiverEndpoint {
Expand Down
19 changes: 19 additions & 0 deletions app/assets/js/components/Work/work.gql.mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import {
GET_WORK,
GET_WORK_TYPES,
VERIFY_FILE_SETS,
TRANSFER_FILE_SETS,
WORK_ARCHIVER_ENDPOINT,
} from "@js/components/Work/work.gql.js";
import { mockVisibility, mockWorkType } from "@js/client-local";

import { GET_IIIF_MANIFEST_HEADERS } from "./work.gql";

export const MOCK_WORK_ID = "ABC123";
export const MOCK_WORK_ID_2 = "DEF456";

export const mockWork = {
id: MOCK_WORK_ID,
Expand Down Expand Up @@ -501,6 +503,23 @@ export const deleteFilesetMock = {
},
};

export const transferFileSetsMock = {
request: {
query: TRANSFER_FILE_SETS,
variables: {
fromWorkId: MOCK_WORK_ID,
toWorkId: MOCK_WORK_ID_2
},
},
result: {
data: {
transferFileSets: {
id: MOCK_WORK_ID,
},
},
},
}

export const verifyFileSetsMock = {
request: {
query: VERIFY_FILE_SETS,
Expand Down
18 changes: 18 additions & 0 deletions app/lib/meadow/data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,24 @@ defmodule Meadow.Data do
|> Repo.one()
end

@doc """
Query returning a flattened list of FileSets for a Work grouped by rank.
## Examples
iex> ranked_file_sets_for_work("01DT7V79D45B8BQMVS6YDRSF9J")
[%Meadow.Data.Schemas.FileSet{rank: -100}, %Meadow.Data.Schemas.FileSet{rank: 0}, %Meadow.Data.Schemas.FileSet{rank: 100}]
iex> ranked_file_sets_for_work(Ecto.UUID.generate())
[]
"""
def ranked_file_sets_for_work(work_id) do
Enum.flat_map(["A", "P", "S", "X"], fn role ->
ranked_file_sets_for_work(work_id, role)
end)
end

@doc """
Query returning a list of FileSets for a Work ordered by `:rank`.
Expand Down
Loading

0 comments on commit c49e428

Please sign in to comment.