From b9139ab33a5822c7e401fe6267d7570ddabfeefa Mon Sep 17 00:00:00 2001 From: Justin Littman Date: Mon, 8 Aug 2022 14:18:08 -0700 Subject: [PATCH] Support local IDs for overlay of MARC records. refs #3555 --- .../local_admin_metadata.json | 55 ++++++++ .../actionCreators/relationships.test.js | 1 + .../resources.loadResource.test.js | 21 +-- __tests__/actionCreators/transfer.test.js | 41 +++++- __tests__/feature/editing/transfer.test.js | 33 ++++- .../feature/editing/viewRelationships.test.js | 2 + .../feature/searchAndPreviewResource.test.js | 3 +- __tests__/reducers/resources.test.js | 6 + __tests__/reducers/transfer.test.js | 109 +++++++++++++++ __tests__/selectors/relationships.test.js | 6 + __tests__/selectors/transfer.test.js | 21 +++ __tests__/sinopiaApi.test.js | 26 +++- .../testUtilities/fixtureLoaderHelper.js | 4 + src/Config.js | 12 +- src/actionCreators/relationships.js | 6 +- src/actionCreators/resources.js | 19 ++- src/actionCreators/transfer.js | 70 +++++++++- src/actions/transfer.js | 9 ++ .../editor/actions/TransferButton.jsx | 124 ++++++++++++++++-- .../editor/actions/TransferButtons.jsx | 10 +- src/reducers/index.js | 3 + src/reducers/resources.js | 2 + src/reducers/transfer.js | 36 +++++ src/selectors/relationships.js | 6 + src/selectors/transfer.js | 4 + src/sinopiaApi.js | 10 +- src/store.js | 1 + 27 files changed, 590 insertions(+), 50 deletions(-) create mode 100644 __tests__/__resource_fixtures__/local_admin_metadata.json create mode 100644 __tests__/reducers/transfer.test.js create mode 100644 __tests__/selectors/transfer.test.js create mode 100644 src/actions/transfer.js create mode 100644 src/reducers/transfer.js create mode 100644 src/selectors/transfer.js diff --git a/__tests__/__resource_fixtures__/local_admin_metadata.json b/__tests__/__resource_fixtures__/local_admin_metadata.json new file mode 100644 index 000000000..701e72b92 --- /dev/null +++ b/__tests__/__resource_fixtures__/local_admin_metadata.json @@ -0,0 +1,55 @@ +[ + { + "@id": "http://localhost:3000/resource/ae93cff4-d272-43b2-a4ee-fb8651907e51", + "@type": [ + "http://sinopia.io/vocabulary/LocalAdminMetadata" + ], + "http://id.loc.gov/ontologies/bibframe/identifier": [ + { + "@id": "_:Nfe4b29df32004cc1b097a218f69df09f" + } + ], + "http://sinopia.io/vocabulary/exportDate": [ + { + "@value": "2022-08-01T15:49:44.558203" + } + ], + "http://sinopia.io/vocabulary/hasResourceTemplate": [ + { + "@value": "pcc:sinopia:localAdminMetadata" + } + ], + "http://sinopia.io/vocabulary/localAdminMetadataFor": [ + { + "@id": "http://localhost:3000/resource/a5c5f4c0-e7cd-4ca5-a20f-2a37fe1080d5" + } + ] + }, + { + "@id": "_:Nfe4b29df32004cc1b097a218f69df09f", + "@type": [ + "http://id.loc.gov/ontologies/bibframe/Local" + ], + "http://id.loc.gov/ontologies/bibframe/source": [ + { + "@id": "_:Nf65f353d6fb64adeb6aa6040d21fb88c" + } + ], + "http://www.w3.org/1999/02/22-rdf-syntax-ns#value": [ + { + "@value": "13714202" + } + ] + }, + { + "@id": "_:Nf65f353d6fb64adeb6aa6040d21fb88c", + "@type": [ + "http://id.loc.gov/ontologies/bibframe/Source" + ], + "http://www.w3.org/2000/01/rdf-schema#label": [ + { + "@value": "SIRSI" + } + ] + } + ] \ No newline at end of file diff --git a/__tests__/actionCreators/relationships.test.js b/__tests__/actionCreators/relationships.test.js index 1a6444f1c..f78668288 100644 --- a/__tests__/actionCreators/relationships.test.js +++ b/__tests__/actionCreators/relationships.test.js @@ -71,6 +71,7 @@ describe("loadRelationships()", () => { bfAdminMetadataRefs: [ "http://localhost:3000/resource/72f2f457-31f5-432c-8acf-b4037f7754g", ], + sinopiaLocalAdminMetadataRefs: [], bfItemRefs: [], bfInstanceRefs: [], bfWorkRefs: [ diff --git a/__tests__/actionCreators/resources.loadResource.test.js b/__tests__/actionCreators/resources.loadResource.test.js index 208639d68..afae1319f 100644 --- a/__tests__/actionCreators/resources.loadResource.test.js +++ b/__tests__/actionCreators/resources.loadResource.test.js @@ -47,7 +47,6 @@ describe("loadResource", () => { sinopiaSearch.getSearchResultsByUris = jest .fn() .mockResolvedValue({ results: [] }) - jest.spyOn(relationshipActionCreators, "loadRelationships") it("dispatches actions", async () => { const result = await store.dispatch( @@ -75,6 +74,19 @@ describe("loadResource", () => { group: "stanford", modified: "2020-08-20T11:34:40.887Z", }) + expect(actions).toHaveAction("SET_RELATIONSHIPS", { + resourceKey: "abc123", + relationships: { + bfAdminMetadataRefs: [], + sinopiaLocalAdminMetadataRefs: [ + "http://localhost:3000/resource/ae93cff4-d272-43b2-a4ee-fb8651907e51", + ], + bfItemRefs: [], + bfInstanceRefs: [], + bfWorkRefs: [], + }, + }) + expect(actions).toHaveAction("CLEAR_LOCAL_IDS", "abc123") expect(sinopiaApi.putUserHistory).toHaveBeenCalledWith( "Foo McBar", @@ -82,13 +94,6 @@ describe("loadResource", () => { "87d27b05d48874c9f80cd4b7e8fc0dcc", uri ) - - // loadRelationships is invoked async and do not wait for results - expect(relationshipActionCreators.loadRelationships).toHaveBeenCalledWith( - "abc123", - uri, - "testerrorkey" - ) }) }) diff --git a/__tests__/actionCreators/transfer.test.js b/__tests__/actionCreators/transfer.test.js index 63a026df8..d5292dccd 100644 --- a/__tests__/actionCreators/transfer.test.js +++ b/__tests__/actionCreators/transfer.test.js @@ -3,36 +3,42 @@ import configureMockStore from "redux-mock-store" import thunk from "redux-thunk" import * as sinopiaApi from "sinopiaApi" import { createState } from "stateUtils" -import { transfer } from "actionCreators/transfer" +import { transfer, loadLocalIds } from "actionCreators/transfer" +import Config from "Config" const mockStore = configureMockStore([thunk]) const resourceUri = "https://api.development.sinopia.io/resource/7b4c275d-b0c7-40a4-80b3-e95a0d9d987c" +// This forces Sinopia server to use fixtures +jest.spyOn(Config, "useResourceTemplateFixtures", "get").mockReturnValue(true) + describe("transfer", () => { describe("successful", () => { it("dispatches actions to add user", async () => { sinopiaApi.postTransfer = jest.fn().mockResolvedValue() const store = mockStore(createState()) await store.dispatch( - transfer(resourceUri, "stanford", "ils", "testerrorkey") + transfer(resourceUri, "stanford", "FOLIO", "abc123", "testerrorkey") ) expect(store.getActions()).toHaveLength(0) expect(sinopiaApi.postTransfer).toHaveBeenCalledWith( resourceUri, "stanford", - "ils" + "FOLIO", + "abc123" ) }) }) - describe("failure", () => { + + describe("transfer failure", () => { it("dispatches actions to remove user", async () => { sinopiaApi.postTransfer = jest.fn().mockRejectedValue("Ooops!") const store = mockStore(createState()) await store.dispatch( - transfer(resourceUri, "stanford", "ils", "testerrorkey") + transfer(resourceUri, "stanford", "", "abc123", "testerrorkey") ) expect(store.getActions()).toHaveAction("ADD_ERROR", { @@ -41,4 +47,29 @@ describe("transfer", () => { }) }) }) + + describe("loadLocalIds()", () => { + it("dispatches actions to set local id", async () => { + const store = mockStore(createState()) + await store.dispatch( + loadLocalIds( + "abc123", + [ + "http://localhost:3000/resource/ae93cff4-d272-43b2-a4ee-fb8651907e51", + ], + "testerrorkey" + ) + ) + + const actions = store.getActions() + + expect(actions).toHaveAction("CLEAR_LOCAL_IDS", "abc123") + expect(actions).toHaveAction("SET_LOCAL_ID", { + resourceKey: "abc123", + target: "SIRSI", + group: "stanford", + localId: "13714202", + }) + }) + }) }) diff --git a/__tests__/feature/editing/transfer.test.js b/__tests__/feature/editing/transfer.test.js index fb5dee059..bd4b009ea 100644 --- a/__tests__/feature/editing/transfer.test.js +++ b/__tests__/feature/editing/transfer.test.js @@ -20,7 +20,7 @@ jest.spyOn(Config, "transferConfig", "get").mockReturnValue({ }, }) -describe("transfer saved bf:Instance when user belongs to a transfer group", () => { +describe("transfer saved bf:Instance when user belongs to a transfer group and no local ID", () => { it("allows transfer", async () => { const state = createState() const store = createStore(state) @@ -42,6 +42,37 @@ describe("transfer saved bf:Instance when user belongs to a transfer group", () const transferBtn = screen.getByText("Export to Catalog") fireEvent.click(transferBtn) + fireEvent.click(await screen.findByText("Create a new record in catalog.")) + await screen.findByText("Requesting") + }, 15000) +}) + +describe("transfer saved bf:Instance when user belongs to a transfer group and provided local ID", () => { + it("allows transfer", async () => { + const state = createState() + const store = createStore(state) + renderApp(store) + + fireEvent.click(screen.getByText("Linked Data Editor", { selector: "a" })) + + fireEvent.change(screen.getByLabelText("Search"), { + target: { value: bfUri }, + }) + fireEvent.click(screen.getByTestId("Submit search")) + + await screen.findByText(bfUri) + fireEvent.click(screen.getByRole("button", { name: `Edit ${bfUri}` })) + + await screen.findByText("The Practitioner's Guide to Graph Data", { + selector: resourceHeaderSelector, + }) + + const transferBtn = screen.getByText("Export to Catalog") + fireEvent.click(transferBtn) + fireEvent.change(await screen.findByLabelText("Enter local system id"), { + target: { value: "abc123" }, + }) + fireEvent.click(await screen.findByText("Go")) await screen.findByText("Requesting") }, 15000) }) diff --git a/__tests__/feature/editing/viewRelationships.test.js b/__tests__/feature/editing/viewRelationships.test.js index 7d980d08a..8a639dae1 100644 --- a/__tests__/feature/editing/viewRelationships.test.js +++ b/__tests__/feature/editing/viewRelationships.test.js @@ -20,6 +20,7 @@ describe("relationships", () => { ], bfInstanceInferredRefs: [], bfWorkInferredRefs: [], + sinopiaHasLocalAdminMetadataInferredRefs: [], }) jest.spyOn(sinopiaSearch, "getSearchResultsByUris").mockResolvedValue({ @@ -75,6 +76,7 @@ describe("relationships", () => { bfItemInferredRefs: [], bfInstanceInferredRefs: [], bfWorkInferredRefs: [], + sinopiaHasLocalAdminMetadataInferredRefs: [], }) }) diff --git a/__tests__/feature/searchAndPreviewResource.test.js b/__tests__/feature/searchAndPreviewResource.test.js index c6ab6396f..d6027ed97 100644 --- a/__tests__/feature/searchAndPreviewResource.test.js +++ b/__tests__/feature/searchAndPreviewResource.test.js @@ -246,7 +246,8 @@ describe("searching and preview a resource", () => { ).toHaveLength(1) screen.getByText("MARC", { selector: "button" }) - screen.getByText(/Export to/, { selector: "button" }) + screen.getByText("Export to Symphony", { selector: "button" }) + screen.getByText("Export to Folio", { selector: "button" }) // Switch to relationships fireEvent.change(screen.getByLabelText(/Format/), { diff --git a/__tests__/reducers/resources.test.js b/__tests__/reducers/resources.test.js index 6fa201694..9d05dd3cb 100644 --- a/__tests__/reducers/resources.test.js +++ b/__tests__/reducers/resources.test.js @@ -940,6 +940,11 @@ describe("clearResource()", () => { bfInstanceRefs: [], bfWorkRefs: [], } + oldState.entities.localIds.t9zVwg2zO = { + FOLIO: { + stanford: "123456", + }, + } const action = { type: "CLEAR_RESOURCE", @@ -952,6 +957,7 @@ describe("clearResource()", () => { expect(Object.keys(newState.values)).toHaveLength(0) expect(Object.keys(newState.versions)).toHaveLength(0) expect(Object.keys(newState.relationships)).toHaveLength(0) + expect(Object.keys(newState.localIds)).toHaveLength(0) }) }) diff --git a/__tests__/reducers/transfer.test.js b/__tests__/reducers/transfer.test.js new file mode 100644 index 000000000..9b5850019 --- /dev/null +++ b/__tests__/reducers/transfer.test.js @@ -0,0 +1,109 @@ +import { clearLocalIds, setLocalId } from "reducers/transfer" +import { createReducer } from "reducers/index" + +const handlers = { + CLEAR_LOCAL_IDS: clearLocalIds, + SET_LOCAL_ID: setLocalId, +} +const reducer = createReducer(handlers) + +describe("clearLocalIds", () => { + it("removes local ids for resource", () => { + const oldState = { + localIds: { + abc123: { + FOLIO: { + stanford: "123456", + }, + }, + def456: { + FOLIO: { + stanford: "234567", + }, + }, + }, + } + const action = { + type: "CLEAR_LOCAL_IDS", + payload: "abc123", + } + + const newState = reducer(oldState, action) + expect(newState).toStrictEqual({ + localIds: { + def456: { + FOLIO: { + stanford: "234567", + }, + }, + }, + }) + }) +}) + +describe("setLocalId", () => { + describe("when resource does not have existing local ids", () => { + it("adds local ids for resource", () => { + const oldState = { + localIds: {}, + } + const action = { + type: "SET_LOCAL_ID", + payload: { + resourceKey: "abc123", + target: "FOLIO", + group: "stanford", + localId: "123456", + }, + } + + const newState = reducer(oldState, action) + expect(newState).toStrictEqual({ + localIds: { + abc123: { + FOLIO: { + stanford: "123456", + }, + }, + }, + }) + }) + }) + + describe("when resource has existing local ids", () => { + it("appends local ids for resource", () => { + const oldState = { + localIds: { + abc123: { + SYMPHONY: { + stanford: "234567", + }, + }, + }, + } + const action = { + type: "SET_LOCAL_ID", + payload: { + resourceKey: "abc123", + target: "FOLIO", + group: "stanford", + localId: "123456", + }, + } + + const newState = reducer(oldState, action) + expect(newState).toStrictEqual({ + localIds: { + abc123: { + SYMPHONY: { + stanford: "234567", + }, + FOLIO: { + stanford: "123456", + }, + }, + }, + }) + }) + }) +}) diff --git a/__tests__/selectors/relationships.test.js b/__tests__/selectors/relationships.test.js index b4fd10122..3ec5d9670 100644 --- a/__tests__/selectors/relationships.test.js +++ b/__tests__/selectors/relationships.test.js @@ -29,6 +29,9 @@ describe("selectRelationships()", () => { "http://localhost:3000/resource/252b24cb-0b5f-4df6-88d2-cb9efdf3f376", ], bfWorkRefs: [], + sinopiaHasLocalAdminMetadataInferredRefs: [ + "http://localhost:3000/resource/362b24cb-0b5f-4df6-88d2-cb9efdf3f377", + ], } expect(selectRelationships(state, "8VrbxGPeF")).toStrictEqual({ @@ -43,6 +46,9 @@ describe("selectRelationships()", () => { "http://localhost:3000/resource/252b24cb-0b5f-4df6-88d2-cb9efdf3f376", ], bfWorkRefs: [], + sinopiaLocalAdminMetadataRefs: [ + "http://localhost:3000/resource/362b24cb-0b5f-4df6-88d2-cb9efdf3f377", + ], }) }) }) diff --git a/__tests__/selectors/transfer.test.js b/__tests__/selectors/transfer.test.js new file mode 100644 index 000000000..0cebe8341 --- /dev/null +++ b/__tests__/selectors/transfer.test.js @@ -0,0 +1,21 @@ +import { createState } from "stateUtils" +import { selectLocalId } from "selectors/transfer" + +describe("selectLocalId()", () => { + it("is nil when no local id", () => { + const state = createState() + expect(selectLocalId(state, "abc123", "FOLIO", "stanford")).toBe(undefined) + }) + + it("returns local id", () => { + const state = createState() + state.entities.localIds = { + abc123: { + FOLIO: { + stanford: "123456", + }, + }, + } + expect(selectLocalId(state, "abc123", "FOLIO", "stanford")).toBe("123456") + }) +}) diff --git a/__tests__/sinopiaApi.test.js b/__tests__/sinopiaApi.test.js index 548f19d81..22e4275a6 100644 --- a/__tests__/sinopiaApi.test.js +++ b/__tests__/sinopiaApi.test.js @@ -431,13 +431,13 @@ describe("putUserHistory", () => { }) describe("postTransfer", () => { - describe("success", () => { + describe("success without localId", () => { it("returns", async () => { global.fetch = jest.fn().mockResolvedValue({ ok: true, }) - await postTransfer(resourceUri, "stanford", "ils") + await postTransfer(resourceUri, "stanford", "ils", null) expect(global.fetch).toHaveBeenCalledWith( "https://api.development.sinopia.io/transfer/7b4c275d-b0c7-40a4-80b3-e95a0d9d987c/stanford/ils", @@ -450,6 +450,25 @@ describe("postTransfer", () => { ) }) }) + describe("success with localId", () => { + it("returns", async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + }) + + await postTransfer(resourceUri, "stanford", "ils", "abc123") + + expect(global.fetch).toHaveBeenCalledWith( + "https://api.development.sinopia.io/transfer/7b4c275d-b0c7-40a4-80b3-e95a0d9d987c/stanford/ils/abc123", + { + method: "POST", + headers: { + Authorization: "Bearer Secret-Token", + }, + } + ) + }) + }) }) describe("fetchResourceRelationships", () => { @@ -467,6 +486,9 @@ describe("fetchResourceRelationships", () => { bfItemInferredRefs: [], bfInstanceInferredRefs: [], bfWorkInferredRefs: [], + sinopiaHasLocalAdminMetadataInferredRefs: [ + "http://localhost:3000/resource/ae93cff4-d272-43b2-a4ee-fb8651907e51", + ], }) }) }) diff --git a/__tests__/testUtilities/fixtureLoaderHelper.js b/__tests__/testUtilities/fixtureLoaderHelper.js index 0aa9da865..f26f56ed5 100644 --- a/__tests__/testUtilities/fixtureLoaderHelper.js +++ b/__tests__/testUtilities/fixtureLoaderHelper.js @@ -15,6 +15,7 @@ const resourceFilenames = { "a5c5f4c0-e7cd-4ca5-a20f-2a37fe1080d5": "instance_with_refs.json", "b6c5f4c0-e7cd-4ca5-a20f-2a37fe1080d6": "test-inputs.json", "c7c5f4c0-e7cd-4ca5-a20f-2a37fe1080d7": "test-multiple_property_uris.json", + "ae93cff4-d272-43b2-a4ee-fb8651907e51": "local_admin_metadata.json", } const templateFilenames = { @@ -201,5 +202,8 @@ export const getFixtureResourceRelationships = () => { bfItemInferredRefs: [], bfInstanceInferredRefs: [], bfWorkInferredRefs: [], + sinopiaHasLocalAdminMetadataInferredRefs: [ + "http://localhost:3000/resource/ae93cff4-d272-43b2-a4ee-fb8651907e51", + ], } } diff --git a/src/Config.js b/src/Config.js index 510c080aa..5b9a298c3 100644 --- a/src/Config.js +++ b/src/Config.js @@ -140,13 +140,19 @@ class Config { static get transferConfig() { return { - ils: { + SIRSI: { // group: label - stanford: "Catalog", + stanford: "Symphony", cornell: "Catalog", + }, + FOLIO: { + stanford: "Folio", + cornell: "Catalog", + }, + ALMA: { penn: "Catalog", }, - // Can add additional transfer targets, e.g., discovery + // Can add additional transfer targets. } } diff --git a/src/actionCreators/relationships.js b/src/actionCreators/relationships.js index 6b652aa98..227c344bd 100644 --- a/src/actionCreators/relationships.js +++ b/src/actionCreators/relationships.js @@ -6,7 +6,7 @@ import { fetchResourceRelationships } from "sinopiaApi" /** * A thunk that loads inferred relationships from the Sinopia API and adds to state. - * @return true if successful + * @return relationships if successful or false if not */ export const loadRelationships = (resourceKey, uri, errorKey) => (dispatch) => { dispatch(clearErrors(errorKey)) @@ -15,12 +15,14 @@ export const loadRelationships = (resourceKey, uri, errorKey) => (dispatch) => { dispatch( setRelationships(resourceKey, { bfAdminMetadataRefs: relationships.bfAdminMetadataInferredRefs, + sinopiaLocalAdminMetadataRefs: + relationships.sinopiaHasLocalAdminMetadataInferredRefs, bfItemRefs: relationships.bfItemInferredRefs, bfInstanceRefs: relationships.bfInstanceInferredRefs, bfWorkRefs: relationships.bfWorkInferredRefs, }) ) - return true + return relationships }) .catch((err) => { console.error(err) diff --git a/src/actionCreators/resources.js b/src/actionCreators/resources.js index c7b1a4f62..70dc432e3 100644 --- a/src/actionCreators/resources.js +++ b/src/actionCreators/resources.js @@ -41,6 +41,7 @@ import { addResourceHistory } from "actionCreators/history" import _ from "lodash" import { setCurrentComponent } from "actions/index" import { loadRelationships } from "./relationships" +import { loadLocalIds } from "./transfer" /** * A thunk that loads an existing resource from Sinopia API and adds to state. @@ -72,7 +73,23 @@ export const loadResource = unusedDataset.size > 0 ? unusedDataset.toCanonical() : null ) ) - dispatch(loadRelationships(resource.key, uri, errorKey)) + dispatch(loadRelationships(resource.key, uri, errorKey)).then( + (relationships) => { + if ( + !_.isEmpty( + relationships.sinopiaHasLocalAdminMetadataInferredRefs + ) + ) { + dispatch( + loadLocalIds( + resource.key, + relationships.sinopiaHasLocalAdminMetadataInferredRefs, + errorKey + ) + ) + } + } + ) return [response, resource, unusedDataset] }) .catch((err) => { diff --git a/src/actionCreators/transfer.js b/src/actionCreators/transfer.js index 333e683f8..a8f710cd7 100644 --- a/src/actionCreators/transfer.js +++ b/src/actionCreators/transfer.js @@ -1,13 +1,75 @@ -import { postTransfer } from "../sinopiaApi" +import { postTransfer, fetchResource } from "../sinopiaApi" import { addError } from "actions/errors" +import { clearLocalIds, setLocalId } from "actions/transfer" +import rdf from "rdf-ext" export const transfer = - (resourceUri, group, target, errorKey) => (dispatch) => { - postTransfer(resourceUri, group, target).catch((err) => { + (resourceUri, group, target, localId, errorKey) => (dispatch) => + postTransfer(resourceUri, group, target, localId).catch((err) => { dispatch( addError(errorKey, `Error requesting transfer: ${err.message || err}`) ) }) + +export const loadLocalIds = + (resourceKey, sinopiaLocalAdminMetadataRefs, errorKey) => (dispatch) => { + dispatch(clearLocalIds(resourceKey)) + return Promise.all( + sinopiaLocalAdminMetadataRefs.map((resourceUri) => dispatch(fetchLocalId(resourceUri, errorKey)).then( + ([target, group, localId]) => { + if (!target) { + return Promise.resolve() + } + return dispatch(setLocalId(resourceKey, target, group, localId)) + } + )) + ) } -export const noop = () => {} +const fetchLocalId = (uri, errorKey) => (dispatch) => + fetchResource(uri) + .then(([dataset, response]) => { + if (!dataset) return [false, false, false] + const identifierNode = identifierNodeFromDataset(uri, dataset) + if (!identifierNode) return [false, false, false] + const localId = localIdFromIdentifierNode(identifierNode, dataset) + const target = targetFromIdentifierNode(identifierNode, dataset) + return [target, response.group, localId] + }) + .catch((err) => { + dispatch( + addError(errorKey, `Error retrieving ${uri}: ${err.message || err}`) + ) + return [false, false, false] + }) + +const localIdFromIdentifierNode = (identifierNode, dataset) => + dataset + .match( + identifierNode, + rdf.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#value") + ) + .toArray()[0].object.value + +const targetFromIdentifierNode = (identifierNode, dataset) => { + const sourceNode = dataset + .match( + identifierNode, + rdf.namedNode("http://id.loc.gov/ontologies/bibframe/source") + ) + .toArray()[0].object + return dataset + .match( + sourceNode, + rdf.namedNode("http://www.w3.org/2000/01/rdf-schema#label") + ) + .toArray()[0].object.value +} + +const identifierNodeFromDataset = (uri, dataset) => + dataset + .match( + rdf.namedNode(uri), + rdf.namedNode("http://id.loc.gov/ontologies/bibframe/identifier") + ) + .toArray()[0].object diff --git a/src/actions/transfer.js b/src/actions/transfer.js new file mode 100644 index 000000000..779deab88 --- /dev/null +++ b/src/actions/transfer.js @@ -0,0 +1,9 @@ +export const clearLocalIds = (resourceKey) => ({ + type: "CLEAR_LOCAL_IDS", + payload: resourceKey, +}) + +export const setLocalId = (resourceKey, target, group, localId) => ({ + type: "SET_LOCAL_ID", + payload: { resourceKey, target, group, localId }, +}) diff --git a/src/components/editor/actions/TransferButton.jsx b/src/components/editor/actions/TransferButton.jsx index 1d5a8936f..0e5cd1bc2 100644 --- a/src/components/editor/actions/TransferButton.jsx +++ b/src/components/editor/actions/TransferButton.jsx @@ -1,8 +1,21 @@ import React, { useState, useEffect, useRef } from "react" +import { useSelector } from "react-redux" +import { selectLocalId } from "selectors/transfer" import PropTypes from "prop-types" +import _ from "lodash" -const TransferButton = ({ label, handleClick }) => { - const [btnText, setBtnText] = useState(label) +const TransferButton = ({ + label, + group, + target, + resourceKey, + handleTransfer, +}) => { + const [requesting, setRequesting] = useState(false) + const localId = useSelector((state) => + selectLocalId(state, resourceKey, target, group) + ) + const [providedLocalId, setProvidedLocalId] = useState(localId) const timerRef = useRef(null) useEffect( @@ -12,27 +25,110 @@ const TransferButton = ({ label, handleClick }) => { [] ) - const handleBtnClick = (event) => { - setBtnText(Requesting) - timerRef.current = setTimeout(() => setBtnText(label), 3000) - handleClick(event) + const handleExistingLocalIdClick = (event) => { + handleTransfer(localId) + notify() + event.preventDefault() + } + + const handleProvidedLocalIdClick = (event) => { + handleTransfer(providedLocalId) + notify() event.preventDefault() } + const handleNoLocalIdClick = (event) => { + handleTransfer(null) + notify() + event.preventDefault() + } + + const notify = () => { + setRequesting(true) + timerRef.current = setTimeout(() => setRequesting(false), 3000) + } + + const handleChangeProvidedLocalId = (event) => { + setProvidedLocalId(event.target.value) + event.preventDefault() + } + + if (requesting) { + return ( + + ) + } + + const btnId = `transferBtn-${group}-${target}` + const btnClasses = ["btn", "dropdown-toggle", "btn-no-outline"] + const dropDownItemBtnClasses = ["btn", "btn-secondary", "dropdown-item"] + return ( - +
+ +
+ {localId ? ( + + + + ) : ( + +
+ Overlay record with local system ID: +
+ + +
+
+ +
+ )} +
+
) } TransferButton.propTypes = { label: PropTypes.string.isRequired, - handleClick: PropTypes.func.isRequired, + group: PropTypes.string.isRequired, + target: PropTypes.string.isRequired, + resourceKey: PropTypes.string.isRequired, + handleTransfer: PropTypes.func.isRequired, } export default TransferButton diff --git a/src/components/editor/actions/TransferButtons.jsx b/src/components/editor/actions/TransferButtons.jsx index 9d74f8144..c51badb4f 100644 --- a/src/components/editor/actions/TransferButtons.jsx +++ b/src/components/editor/actions/TransferButtons.jsx @@ -35,16 +35,18 @@ const TransferButtons = ({ resourceKey }) => { // Must be targets if (_.isEmpty(transferTargets)) return null - const handleClick = (event, group, target) => { - dispatch(transfer(resource.uri, group, target, errorKey)) - event.preventDefault() + const handleTransfer = (group, target, localId) => { + dispatch(transfer(resource.uri, group, target, localId, errorKey)) } const buttons = transferTargets.map(([target, group, label]) => ( handleClick(event, group, target)} + handleTransfer={(localId) => handleTransfer(group, target, localId)} /> )) diff --git a/src/reducers/index.js b/src/reducers/index.js index 7c417746d..3ad5eacd9 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -60,6 +60,7 @@ import { } from "./history" import { clearSearchResults, setSearchResults, setHeaderSearch } from "./search" import { lookupOptionsRetrieved } from "./lookups" +import { clearLocalIds, setLocalId } from "./transfer" import _ from "lodash" export const setCurrentComponent = (state, action) => { @@ -119,6 +120,7 @@ const entityHandlers = { ADD_SUBJECT: addSubject, ADD_TEMPLATES: addTemplates, ADD_VALUE: addValue, + CLEAR_LOCAL_IDS: clearLocalIds, CLEAR_RELATIONSHIPS: clearRelationships, CLEAR_RESOURCE: clearResource, CLEAR_VERSIONS: clearVersions, @@ -137,6 +139,7 @@ const entityHandlers = { SET_BASE_URL: setBaseURL, SET_CLASSES: setClasses, SET_DEFAULT_LANG: setDefaultLang, + SET_LOCAL_ID: setLocalId, SET_VALUE_PROPERTY_URI: setValuePropertyURI, SET_PROPERTY_PROPERTY_URI: setPropertyPropertyURI, SET_RELATIONSHIPS: setRelationships, diff --git a/src/reducers/resources.js b/src/reducers/resources.js index fb193d7af..3a92f28b4 100644 --- a/src/reducers/resources.js +++ b/src/reducers/resources.js @@ -28,6 +28,7 @@ import { import { clearRelationships } from "./relationships" import { resourceEditErrorKey } from "utilities/errorKeyFactory" import Config from "Config" +import { clearLocalIds } from "./transfer" export const setBaseURL = (state, action) => mergeSubjectPropsToNewState(state, action.payload.resourceKey, { @@ -474,6 +475,7 @@ export const clearResourceFromEditor = (state, action) => { export const clearResource = (state, action) => { let newState = clearSubjectFromNewState(state, action.payload) newState = clearVersions(newState, action) + newState = clearLocalIds(newState, action) return clearRelationships(newState, action) } diff --git a/src/reducers/transfer.js b/src/reducers/transfer.js new file mode 100644 index 000000000..6a802a422 --- /dev/null +++ b/src/reducers/transfer.js @@ -0,0 +1,36 @@ +// Copyright 2020 Stanford University see LICENSE for license + +export const addError = (state, action) => ({ + ...state, + errors: { + ...state.errors, + [action.payload.errorKey]: [ + ...(state.errors[action.payload.errorKey] || []), + action.payload.error, + ], + }, +}) + +export const clearLocalIds = (state, action) => { + const newLocalIds = { ...state.localIds } + delete newLocalIds[action.payload] + + return { + ...state, + localIds: newLocalIds, + } +} + +export const setLocalId = (state, action) => ({ + ...state, + localIds: { + ...state.localIds, + [action.payload.resourceKey]: { + ...state.localIds[action.payload.resourceKey], + [action.payload.target]: { + ...state.localIds[action.payload.resourceKey]?.[action.payload.target], + [action.payload.group]: action.payload.localId, + }, + }, + }, +}) diff --git a/src/selectors/relationships.js b/src/selectors/relationships.js index 4532c4b2c..901696b02 100644 --- a/src/selectors/relationships.js +++ b/src/selectors/relationships.js @@ -13,11 +13,17 @@ export const selectRelationships = (state, resourceKey) => { return _.uniq([...resourceRelationships, ...inferredRelationships]) } + const adminMetadataRefs = _.uniq([ + ...(resource?.sinopiaLocalAdminMetadataRefs || []), + ...(relationships?.sinopiaHasLocalAdminMetadataInferredRefs || []), + ]) + return { bfAdminMetadataRefs: mergeRelationship("bfAdminMetadataRefs"), bfItemRefs: mergeRelationship("bfItemRefs"), bfInstanceRefs: mergeRelationship("bfInstanceRefs"), bfWorkRefs: mergeRelationship("bfWorkRefs"), + sinopiaLocalAdminMetadataRefs: adminMetadataRefs, } } diff --git a/src/selectors/transfer.js b/src/selectors/transfer.js new file mode 100644 index 000000000..cf96852c2 --- /dev/null +++ b/src/selectors/transfer.js @@ -0,0 +1,4 @@ +export const selectLocalId = (state, resourceKey, target, group) => + state.entities.localIds[resourceKey]?.[target]?.[group] + +export const noop = () => {} diff --git a/src/sinopiaApi.js b/src/sinopiaApi.js index 2c03e12b9..adfa0a845 100644 --- a/src/sinopiaApi.js +++ b/src/sinopiaApi.js @@ -212,11 +212,11 @@ export const putUserHistory = ( ) } -export const postTransfer = (resourceUri, group, target) => { - const url = `${resourceUri.replace( - "resource", - "transfer" - )}/${group}/${target}` +export const postTransfer = (resourceUri, group, target, localId) => { + let url = `${resourceUri.replace("resource", "transfer")}/${group}/${target}` + if (localId) { + url += `/${localId}` + } return getJwt() .then((jwt) => fetch(url, { diff --git a/src/store.js b/src/store.js index 24649198e..6475f37ba 100644 --- a/src/store.js +++ b/src/store.js @@ -43,6 +43,7 @@ export const initialState = { transliterations: {}, transliterationLookup: [], groupMap: {}, + localIds: {}, // {: {target: {group: localId}}} lookups: {}, exports: [], properties: {},