)
+
+ it('renders the uri component', () => {
+ expect(wrapper.find('Connect(InputURI)').length).toEqual(1)
+ })
+ })
+
describe('when there are no configuration values from the property template', () => {
describe('for unconfigured templates of type:literal', () => {
const template = {
diff --git a/src/components/editor/property/InputURI.jsx b/src/components/editor/property/InputURI.jsx
new file mode 100644
index 000000000..f7ec8930d
--- /dev/null
+++ b/src/components/editor/property/InputURI.jsx
@@ -0,0 +1,195 @@
+// Copyright 2019 Stanford University see LICENSE for license
+
+import React, { useRef, useState } from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import shortid from 'shortid'
+import { removeItem, setItems } from 'actions/index'
+import { findNode, getDisplayValidations, getPropertyTemplate } from 'selectors/resourceSelectors'
+import { booleanPropertyFromTemplate } from 'Utilities'
+
+const InputURI = (props) => {
+ // Don't render if don't have property templates yet.
+ if (!props.propertyTemplate) {
+ return null
+ }
+
+ const inputLiteralRef = useRef(Math.floor(100 * Math.random()))
+ const [content, setContent] = useState('')
+
+ const disabled = !booleanPropertyFromTemplate(props.propertyTemplate, 'repeatable', true)
+ && props.items?.length > 0
+
+ const handleFocus = (event) => {
+ document.getElementById(event.target.id).focus()
+ event.preventDefault()
+ }
+
+ const isValidURI = (value) => {
+ try {
+ /* eslint no-new: 'off' */
+ new URL(value)
+ return true
+ } catch (e) {
+ return false
+ }
+ }
+
+ const addItem = () => {
+ const currentcontent = content.trim()
+
+ if (!currentcontent || !isValidURI(currentcontent)) {
+ return
+ }
+
+ const userInput = {
+ uri: props.propertyTemplate.propertyURI,
+ reduxPath: props.reduxPath,
+ items: [{ uri: currentcontent, id: shortid.generate() }],
+ }
+
+ props.handleMyItemsChange(userInput)
+ setContent('')
+ }
+
+ const handleKeypress = (event) => {
+ if (event.key === 'Enter') {
+ addItem()
+ event.preventDefault()
+ }
+ }
+
+ const handleDeleteClick = (event) => {
+ props.handleRemoveItem(props.reduxPath, event.target.dataset.item)
+ }
+
+ const handleEditClick = (event) => {
+ const idToRemove = event.target.dataset.item
+
+ props.items.forEach((item) => {
+ if (item.id === idToRemove) {
+ const itemContent = item.uri
+
+ setContent(itemContent)
+ }
+ })
+
+ handleDeleteClick(event)
+ inputLiteralRef.current.focus()
+ }
+
+ /**
+ * @return {bool} true if the field should be marked as required (e.g. not all obligations met)
+ */
+ const required = booleanPropertyFromTemplate(props.propertyTemplate, 'mandatory', false)
+ && props.formData.errors
+ && props.formData.errors.length !== 0
+
+ const items = props.items || []
+
+ const addedList = items.map((obj) => {
+ const itemId = obj.id || shortid.generate()
+
+ return
+ {obj.uri}
+
+
+
+ })
+
+ const error = props.displayValidations && required ? 'Required' : undefined
+ let groupClasses = 'form-group'
+
+ if (error) {
+ groupClasses += ' has-error'
+ }
+
+ return (
+
+ setContent(event.target.value)}
+ onKeyPress={handleKeypress}
+ value={content}
+ disabled={disabled}
+ id={props.id}
+ onClick={handleFocus}
+ ref={inputLiteralRef}
+ />
+ {error && {error}}
+ {addedList}
+
+ )
+}
+
+InputURI.propTypes = {
+ id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ propertyTemplate: PropTypes.shape({
+ propertyLabel: PropTypes.string.isRequired,
+ propertyURI: PropTypes.string.isRequired,
+ mandatory: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
+ repeatable: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
+ remark: PropTypes.string,
+ valueConstraint: PropTypes.shape({
+ defaults: PropTypes.array,
+ }),
+ }),
+ formData: PropTypes.shape({
+ id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ uri: PropTypes.string,
+ errors: PropTypes.array,
+ }),
+ items: PropTypes.array,
+ handleMyItemsChange: PropTypes.func,
+ handleRemoveItem: PropTypes.func,
+ reduxPath: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
+ displayValidations: PropTypes.bool,
+}
+
+const mapStateToProps = (state, props) => {
+ const reduxPath = props.reduxPath
+ const resourceTemplateId = reduxPath[reduxPath.length - 2]
+ const propertyURI = reduxPath[reduxPath.length - 1]
+ const displayValidations = getDisplayValidations(state)
+ const formData = findNode(state.selectorReducer, reduxPath)
+ // items has to be its own prop or rerendering won't occur when one is removed
+ const items = formData.items
+ const propertyTemplate = getPropertyTemplate(state, resourceTemplateId, propertyURI)
+
+ return {
+ formData,
+ items,
+ reduxPath,
+ propertyTemplate,
+ displayValidations,
+ }
+}
+
+const mapDispatchToProps = dispatch => ({
+ handleMyItemsChange(userInput) {
+ dispatch(setItems(userInput))
+ },
+ handleRemoveItem(reduxPath, itemId) {
+ dispatch(removeItem(reduxPath, itemId))
+ },
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(InputURI)
diff --git a/src/components/editor/property/PropertyComponent.jsx b/src/components/editor/property/PropertyComponent.jsx
index 007084231..a9b1b97be 100644
--- a/src/components/editor/property/PropertyComponent.jsx
+++ b/src/components/editor/property/PropertyComponent.jsx
@@ -6,6 +6,8 @@ import PropTypes from 'prop-types'
import InputLiteral from './InputLiteral'
import InputListLOC from './InputListLOC'
import InputLookupQA from './InputLookupQA'
+import InputURI from './InputURI'
+
import { getLookupConfigItems } from 'Utilities'
export class PropertyComponent extends Component {
@@ -41,9 +43,15 @@ export class PropertyComponent extends Component {
propertyTemplate = {property}
lookupConfig = {this.state.configuration[0]} />)
default:
- if (property.type === 'literal') {
- return ()
+ switch (property.type) {
+ case 'literal':
+ return ()
+ case 'resource':
+ return ()
+ default:
+ console.error(`Unknown property type (component=${config}, type=${property.type})`)
}
}
diff --git a/src/reducers/inputs.js b/src/reducers/inputs.js
index 61750e2f9..4b3862505 100644
--- a/src/reducers/inputs.js
+++ b/src/reducers/inputs.js
@@ -77,7 +77,6 @@ export const setItemsOrSelections = (state, action) => {
const newState = { ...state }
const reduxPath = action.payload.reduxPath
let level = 0
-
reduxPath.reduce((obj, key) => {
level++
// we've reached the end of the reduxPath, so set the items with the user input
diff --git a/src/sinopiaServerSpoof.js b/src/sinopiaServerSpoof.js
index f77d3442f..f1844e3f8 100644
--- a/src/sinopiaServerSpoof.js
+++ b/src/sinopiaServerSpoof.js
@@ -1,5 +1,6 @@
// Copyright 2018 Stanford University see LICENSE for license
+const onlyEquivalentRt = require('../static/spoofedFilesFromServer/fromSinopiaServer/resourceTemplates/OnlyEquivalent.json')
const barcodeRt = require('../static/spoofedFilesFromServer/fromSinopiaServer/resourceTemplates/Barcode.json')
const monographInstanceRt = require('../static/spoofedFilesFromServer/fromSinopiaServer/resourceTemplates/MonographInstance.json')
const monographWorkRt = require('../static/spoofedFilesFromServer/fromSinopiaServer/resourceTemplates/MonographWork.json')
@@ -28,6 +29,7 @@ export const resourceTemplateId2Json = [
{ id: 'resourceTemplate:bf2:Monograph:Instance', json: monographInstanceRt },
{ id: 'resourceTemplate:bf2:Monograph:Work', json: monographWorkRt },
{ id: 'resourceTemplate:bf2:Identifiers:Barcode', json: barcodeRt },
+ { id: 'resourceTemplate:bf2:OnlyEquivalent', json: onlyEquivalentRt },
{ id: 'resourceTemplate:bf2:Note', json: noteRt },
{ id: 'resourceTemplate:bf2:ParallelTitle', json: parallelTitleRt },
{ id: 'resourceTemplate:bf2:Title', json: titleRt },
diff --git a/static/spoofedFilesFromServer/fromSinopiaServer/resourceTemplates/Item.json b/static/spoofedFilesFromServer/fromSinopiaServer/resourceTemplates/Item.json
index 36e475130..37176251a 100644
--- a/static/spoofedFilesFromServer/fromSinopiaServer/resourceTemplates/Item.json
+++ b/static/spoofedFilesFromServer/fromSinopiaServer/resourceTemplates/Item.json
@@ -48,6 +48,19 @@
"propertyURI": "http://id.loc.gov/ontologies/bibframe/enumerationAndChronology",
"propertyLabel": "Enumeration and chronology"
},
+ {
+ "mandatory": "false",
+ "repeatable": "true",
+ "type": "resource",
+ "resourceTemplates": [],
+ "valueConstraint": {
+ "valueTemplateRefs": [],
+ "useValuesFrom": [],
+ "valueDataType": {}
+ },
+ "propertyURI": "http://id.loc.gov/ontologies/bibframe/hasEquivalent",
+ "propertyLabel": "Equivalent"
+ },
{
"mandatory": "true",
"repeatable": "true",
@@ -132,4 +145,4 @@
"remark": "http://access.rdatoolkit.org/rdachp4_rda4-93.html"
}
]
-}
\ No newline at end of file
+}
diff --git a/static/spoofedFilesFromServer/fromSinopiaServer/resourceTemplates/OnlyEquivalent.json b/static/spoofedFilesFromServer/fromSinopiaServer/resourceTemplates/OnlyEquivalent.json
new file mode 100644
index 000000000..7e909e780
--- /dev/null
+++ b/static/spoofedFilesFromServer/fromSinopiaServer/resourceTemplates/OnlyEquivalent.json
@@ -0,0 +1,20 @@
+{
+ "resourceURI": "http://id.loc.gov/ontologies/bibframe/Item",
+ "resourceLabel": "Only the Equivalent",
+ "id": "resourceTemplate:bf2:OnlyEquivalent",
+ "propertyTemplates": [
+ {
+ "mandatory": "false",
+ "repeatable": "true",
+ "type": "resource",
+ "resourceTemplates": [],
+ "valueConstraint": {
+ "valueTemplateRefs": [],
+ "useValuesFrom": [],
+ "valueDataType": {}
+ },
+ "propertyURI": "http://id.loc.gov/ontologies/bibframe/hasEquivalent",
+ "propertyLabel": "Equivalent"
+ }
+ ]
+}