Skip to content

Commit 444a534

Browse files
authored
Merge pull request #21 from blue-core-lod/t6-edit-existing
Edit Existing Blue Core Resources in Sinopia
2 parents b067b81 + 584c425 commit 444a534

File tree

11 files changed

+261
-114
lines changed

11 files changed

+261
-114
lines changed

src/Config.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,14 @@ class Config {
6161
}
6262

6363
static get templateSearchResultsPerPage() {
64-
// This # should be large enough to return all results.
65-
return 250
64+
return 10
6665
}
6766

6867
/*
6968
* This is the public endpont for the sinopia search.
7069
*/
7170
static get searchPath() {
72-
// return "/api/search/sinopia_resources/_search"
73-
return "/api/search"
71+
return process.env.SEARCH_PATH || ""
7472
}
7573

7674
static get templateSearchPath() {

src/actionCreators/relationships.js

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2020 Stanford University see LICENSE for license
22

33
import { setRelationships, setSearchRelationships } from "actions/relationships"
4-
import { clearErrors, addError } from "actions/errors"
4+
import { clearErrors } from "actions/errors"
55
import { fetchResourceRelationships } from "sinopiaApi"
66

77
/**
@@ -23,13 +23,9 @@ export const loadRelationships = (resourceKey, uri, errorKey) => (dispatch) => {
2323
return true
2424
})
2525
.catch((err) => {
26-
console.error(err)
27-
dispatch(
28-
addError(
29-
errorKey,
30-
`Error retrieving relationships for ${uri}: ${err.message || err}`
31-
)
32-
)
26+
// Relationships endpoint is optional and not supported by all APIs (e.g., Blue Core)
27+
// Silently fail without dispatching errors to avoid blocking resource loading
28+
console.warn(`Relationships endpoint not available for ${uri}, skipping relationships loading`)
3329
return false
3430
})
3531
}
@@ -47,12 +43,8 @@ export const loadSearchRelationships = (uri, errorKey) => (dispatch) =>
4743
return true
4844
})
4945
.catch((err) => {
50-
console.error(err)
51-
dispatch(
52-
addError(
53-
errorKey,
54-
`Error retrieving relationships for ${uri}: ${err.message || err}`
55-
)
56-
)
46+
// Relationships endpoint is optional and not supported by all APIs (e.g., Blue Core)
47+
// Silently fail without dispatching errors to avoid blocking resource loading
48+
console.warn(`Relationships endpoint not available for ${uri}, skipping relationships loading`)
5749
return false
5850
})

src/actionCreators/resourceHelpers.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -491,9 +491,13 @@ const selectResourceTemplateId =
491491
resourceTemplatePromises,
492492
errorKey
493493
)
494-
).then((subjectTemplate) =>
495-
subjectTemplate.class === resourceURI ? resourceTemplateId : undefined
496-
)
494+
).then((subjectTemplate) => {
495+
// Check if resourceURI matches either the required class or any optional class
496+
if (!subjectTemplate) return undefined
497+
const allClasses = Object.keys(subjectTemplate.classes || {})
498+
const matches = allClasses.includes(resourceURI)
499+
return matches ? resourceTemplateId : undefined
500+
})
497501
)
498502
)
499503

@@ -674,9 +678,5 @@ const newValueCopy = (valueKey, property) => (dispatch, getState) => {
674678

675679
export const resourceTemplateIdFromDataset = (uri, dataset) => {
676680
const resourceTemplateId = findRootResourceTemplateId(uri, dataset)
677-
if (!resourceTemplateId)
678-
throw new Error(
679-
"A single resource template must be included as a triple (http://sinopia.io/vocabulary/hasResourceTemplate)"
680-
)
681-
return resourceTemplateId
681+
return resourceTemplateId || null
682682
}

src/actionCreators/resources.js

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { addTemplateHistory } from "actions/history"
22
import { clearErrors, addError } from "actions/errors"
3+
import { showModal } from "actions/modals"
4+
import {
5+
setPendingResourceTemplateSelection,
6+
clearPendingResourceTemplateSelection,
7+
} from "actions/resources"
38
import {
49
addResourceFromDataset,
510
addEmptyResource,
@@ -45,19 +50,39 @@ import { setCurrentComponent } from "actions/index"
4550
import { loadRelationships } from "./relationships"
4651
import { useKeycloak } from "../KeycloakContext"
4752

48-
4953
/**
5054
* A thunk that loads an existing resource from Sinopia API and adds to state.
5155
* @return {[resource, unusedDataset]} if successful
5256
*/
5357
export const loadResource =
54-
(uri, errorKey, { asNewResource = false, version = null } = {}) =>
58+
(
59+
uri,
60+
errorKey,
61+
{ asNewResource = false, version = null, keycloak = null } = {}
62+
) =>
5563
(dispatch) => {
5664
dispatch(clearErrors(errorKey))
5765
return fetchResource(uri, { version })
5866
.then(([dataset, response]) => {
5967
if (!dataset) return false
6068
const resourceTemplateId = resourceTemplateIdFromDataset(uri, dataset)
69+
70+
// If no resource template ID, store pending data and show modal
71+
if (!resourceTemplateId) {
72+
dispatch(
73+
setPendingResourceTemplateSelection(
74+
uri,
75+
dataset,
76+
response,
77+
asNewResource,
78+
errorKey,
79+
keycloak
80+
)
81+
)
82+
dispatch(showModal("ResourceTemplateChoiceModal"))
83+
return false
84+
}
85+
6186
return dispatch(
6287
addResourceFromDataset(
6388
dataset,
@@ -105,8 +130,11 @@ export const loadResource =
105130
export const loadResourceForEditor =
106131
(uri, errorKey, { asNewResource = false } = {}, keycloak) =>
107132
(dispatch) =>
108-
dispatch(loadResource(uri, errorKey, { asNewResource })).then((result) =>
109-
dispatch(dispatchResourceForEditor(result, uri, { asNewResource }, keycloak))
133+
dispatch(loadResource(uri, errorKey, { asNewResource, keycloak })).then(
134+
(result) =>
135+
dispatch(
136+
dispatchResourceForEditor(result, uri, { asNewResource }, keycloak)
137+
)
110138
)
111139

112140
export const dispatchResourceForEditor =
@@ -138,6 +166,60 @@ export const dispatchResourceForEditor =
138166
return true
139167
}
140168

169+
/**
170+
* A thunk that completes loading a resource after a template has been selected.
171+
* This is used when a resource doesn't have a template ID and the user selects one via modal.
172+
*/
173+
export const completeResourceLoadingWithTemplate =
174+
(resourceTemplateId) => (dispatch, getState) => {
175+
const pending = getState().editor.pendingResourceTemplateSelection
176+
if (!pending) {
177+
console.error("No pending resource template selection found")
178+
return Promise.resolve(false)
179+
}
180+
181+
const { uri, dataset, response, asNewResource, errorKey, keycloak } =
182+
pending
183+
184+
// Clear pending state
185+
dispatch(clearPendingResourceTemplateSelection())
186+
187+
// Load the resource with the selected template
188+
return dispatch(
189+
addResourceFromDataset(
190+
dataset,
191+
uri,
192+
resourceTemplateId,
193+
errorKey,
194+
asNewResource,
195+
_.pick(response, ["group", "editGroups"])
196+
)
197+
)
198+
.then(([resource, usedDataset]) => {
199+
const unusedDataset = dataset.difference(usedDataset)
200+
dispatch(
201+
setUnusedRDF(
202+
resource.key,
203+
unusedDataset.size > 0 ? unusedDataset.toCanonical() : null
204+
)
205+
)
206+
dispatch(loadRelationships(resource.key, uri, errorKey))
207+
const result = [response, resource, unusedDataset]
208+
return dispatch(
209+
dispatchResourceForEditor(result, uri, { asNewResource }, keycloak)
210+
)
211+
})
212+
.catch((err) => {
213+
if (err.name !== "ResourceTemplateError") {
214+
console.error(err)
215+
dispatch(
216+
addError(errorKey, `Error retrieving ${uri}: ${err.message || err}`)
217+
)
218+
}
219+
return false
220+
})
221+
}
222+
141223
export const loadResourceForPreview =
142224
(uri, errorKey, { version = null } = {}) =>
143225
(dispatch) =>

src/actionCreators/templates.js

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import TemplatesBuilder from "TemplatesBuilder"
88
import { fetchResource } from "sinopiaApi"
99
import { resourceToName } from "../utilities/Utilities"
1010
import { selectUser } from "selectors/authenticate"
11+
import { getTemplateSearchResultsByIds } from "sinopiaSearch"
1112

1213
/**
1314
* A thunk that gets a resource template from state or the server.
@@ -61,22 +62,69 @@ export const loadResourceTemplateWithoutValidation =
6162
return Promise.resolve(subjectTemplate)
6263
}
6364

64-
const id = resourceToName(resourceTemplateId)
65-
const templateUri = `${Config.sinopiaApiBase}/resource/${id}`
65+
// If resourceTemplateId is not a full URI, search for it to get the URI
66+
const isFullUri =
67+
resourceTemplateId.startsWith("http://") ||
68+
resourceTemplateId.startsWith("https://")
6669

67-
const newResourceTemplatePromise = fetchResource(templateUri, {
68-
isTemplate: true,
69-
}).then(([dataset, response]) => {
70-
const user = selectUser(getState())
71-
const subjectTemplate = new TemplatesBuilder(
72-
dataset,
73-
templateUri,
74-
user.username,
75-
response.group,
76-
response.editGroups
77-
).build()
78-
dispatch(addTemplates(subjectTemplate))
79-
return subjectTemplate
70+
const templateUriPromise = isFullUri
71+
? Promise.resolve(resourceTemplateId)
72+
: getTemplateSearchResultsByIds([resourceTemplateId]).then(
73+
(searchResults) => {
74+
if (searchResults.results && searchResults.results.length > 0) {
75+
76+
// Filter results to find the one matching the requested template ID
77+
// This is necessary because Blue Core search returns multiple results
78+
// Try multiple possible field names for the template ID
79+
const matchingResult = searchResults.results.find(
80+
result => result.resourceId === resourceTemplateId ||
81+
result.id === resourceTemplateId ||
82+
result.templateId === resourceTemplateId
83+
)
84+
85+
if (matchingResult && matchingResult.uri) {
86+
const uri = matchingResult.uri
87+
return uri
88+
}
89+
90+
// If no exact match found, log the issue and fall back
91+
console.warn(`Template search for ${resourceTemplateId} returned ${searchResults.results.length} results but none matched`, searchResults.results)
92+
}
93+
94+
// Fallback to legacy URI construction if search fails
95+
const fallbackUri = `${Config.sinopiaApiBase}/resource/${resourceToName(
96+
resourceTemplateId
97+
)}`
98+
console.warn(`Template search failed for ${resourceTemplateId}, using fallback:`, fallbackUri)
99+
return fallbackUri
100+
}
101+
)
102+
103+
const newResourceTemplatePromise = templateUriPromise.then((templateUri) => {
104+
return fetchResource(templateUri, {
105+
isTemplate: true,
106+
}).then(([dataset, response]) => {
107+
const user = selectUser(getState())
108+
const subjectTemplate = new TemplatesBuilder(
109+
dataset,
110+
templateUri,
111+
user.username,
112+
response.group,
113+
response.editGroups
114+
).build()
115+
// Validate that the loaded template matches the requested ID
116+
// Only check when we used search (not a full URI), to work around Blue Core search
117+
if (!isFullUri && subjectTemplate.id !== resourceTemplateId) {
118+
const error = new Error(
119+
`Search returned wrong template: requested ${resourceTemplateId} but got ${subjectTemplate.id} from ${templateUri}. This indicates the Blue Core API search index is misconfigured.`
120+
)
121+
console.error(error.message)
122+
throw error
123+
}
124+
125+
dispatch(addTemplates(subjectTemplate))
126+
return subjectTemplate
127+
})
80128
})
81129

82130
if (resourceTemplatePromises)

src/actions/resources.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,19 @@ export const setResourceChanged = (resourceKey) => ({
174174
type: "SET_RESOURCE_CHANGED",
175175
payload: resourceKey,
176176
})
177+
178+
export const setPendingResourceTemplateSelection = (
179+
uri,
180+
dataset,
181+
response,
182+
asNewResource,
183+
errorKey,
184+
keycloak
185+
) => ({
186+
type: "SET_PENDING_RESOURCE_TEMPLATE_SELECTION",
187+
payload: { uri, dataset, response, asNewResource, errorKey, keycloak },
188+
})
189+
190+
export const clearPendingResourceTemplateSelection = () => ({
191+
type: "CLEAR_PENDING_RESOURCE_TEMPLATE_SELECTION",
192+
})

src/components/InputTemplate.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const InputTemplate = ({
2222
const newOptions = searchResults.results.map((result) => ({
2323
label: `${result.resourceLabel} (${result.id})`,
2424
id: result.id,
25+
uri: result.uri,
2526
}))
2627
setOptions(newOptions)
2728
setLoading(false)
@@ -40,7 +41,8 @@ const InputTemplate = ({
4041
const change = (newSelected) => {
4142
setSelected(newSelected)
4243
if (newSelected.length === 1) {
43-
setTemplateId(newSelected[0].id)
44+
// Pass the URI if available (for Blue Core templates), otherwise pass the ID
45+
setTemplateId(newSelected[0].uri || newSelected[0].id)
4446
}
4547
}
4648

src/components/search/SinopiaSearchResults.jsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,41 @@
22
/* eslint max-params: ["error", 4] */
33

44
import React from "react"
5-
import { useSelector } from "react-redux"
5+
import { useSelector, useDispatch } from "react-redux"
66
import { selectSearchResults } from "selectors/search"
77
import TypeFilter from "./TypeFilter"
88
import GroupFilter from "./GroupFilter"
99
import SearchResultRows from "./SearchResultRows"
1010
import SinopiaSort from "./SinopiaSort"
1111
import MarcModal from "../editor/actions/MarcModal"
12+
import ResourceTemplateChoiceModal from "../ResourceTemplateChoiceModal"
13+
import { completeResourceLoadingWithTemplate } from "actionCreators/resources"
14+
import { useHistory } from "react-router-dom"
1215
import _ from "lodash"
1316

1417
const SinopiaSearchResults = () => {
18+
const dispatch = useDispatch()
19+
const history = useHistory()
1520
const searchResults = useSelector((state) =>
1621
selectSearchResults(state, "resource")
1722
)
1823

24+
const chooseResourceTemplate = (resourceTemplateId) => {
25+
dispatch(completeResourceLoadingWithTemplate(resourceTemplateId)).then(
26+
(result) => {
27+
if (result) history.push("/editor")
28+
}
29+
)
30+
}
31+
1932
if (_.isEmpty(searchResults)) {
2033
return null
2134
}
2235

2336
return (
2437
<React.Fragment>
2538
<MarcModal />
39+
<ResourceTemplateChoiceModal choose={chooseResourceTemplate} />
2640
<div className="row">
2741
<div className="col" style={{ marginBottom: "5px" }}>
2842
<TypeFilter />

0 commit comments

Comments
 (0)