From 9ffc80807df68362b46f65d9625a11145685529f Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Mon, 25 Apr 2022 15:17:03 -0400 Subject: [PATCH 1/2] UIIN-340: Add ability to add related instance title --- CHANGELOG.md | 1 + package.json | 7 +- src/Instance/InstanceEdit/InstanceEdit.js | 6 +- .../RelatedInstanceField.js | 118 ++++++++++++++++++ .../RelatedInstanceField/index.js | 1 + .../RelatedInstanceFields.js | 52 ++++++++ .../RelatedInstanceFields.test.js | 92 ++++++++++++++ .../RelatedInstanceFields/index.js | 1 + src/edit/InstanceForm.js | 38 ++++-- src/providers/DataProvider.js | 12 ++ src/utils.js | 5 + src/validation.js | 18 +++ test/fixtures/index.js | 2 + test/fixtures/relatedInstanceTypes.js | 18 +++ test/fixtures/relatedInstances.js | 24 ++++ translations/ui-inventory/en.json | 1 + 16 files changed, 385 insertions(+), 11 deletions(-) create mode 100644 src/Instance/InstanceEdit/RelatedInstanceField/RelatedInstanceField.js create mode 100644 src/Instance/InstanceEdit/RelatedInstanceField/index.js create mode 100644 src/Instance/InstanceEdit/RelatedInstanceFields/RelatedInstanceFields.js create mode 100644 src/Instance/InstanceEdit/RelatedInstanceFields/RelatedInstanceFields.test.js create mode 100644 src/Instance/InstanceEdit/RelatedInstanceFields/index.js create mode 100644 test/fixtures/relatedInstanceTypes.js create mode 100644 test/fixtures/relatedInstances.js diff --git a/CHANGELOG.md b/CHANGELOG.md index dfaef0e98..1ea251868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Inventory causes an error. Refs UIIN-2012. * Update locations in `` after edit. Fixes UIIN-1980. * Retrieve up to 5000 locations when viewing Instances. Refs UIIN-2016. * Remove `react-hot-loader`. Refs UIIN-1981. +* Add ability to add related instance title. Refs UIIN-340. ## [9.0.0](https://github.com/folio-org/ui-inventory/tree/v9.0.0) (2022-03-03) [Full Changelog](https://github.com/folio-org/ui-inventory/compare/v8.0.0...v9.0.0) diff --git a/package.json b/package.json index c8ab768d8..ccced43fa 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,8 @@ "configuration": "2.0", "tags": "1.0", "inventory-record-bulk": "1.0", - "data-export": "5.0" + "data-export": "5.0", + "related-instance-types": "1.0" }, "optionalOkapiInterfaces": { "copycat-imports": "1.0", @@ -541,7 +542,9 @@ "orders.collection.get", "orders.item.get", "organizations.organizations.item.get", - "acquisitions-units.units.collection.get" + "acquisitions-units.units.collection.get", + "inventory-storage.related-instance-types.collection.get", + "inventory-storage.related-instance-types.item.get" ], "visible": true }, diff --git a/src/Instance/InstanceEdit/InstanceEdit.js b/src/Instance/InstanceEdit/InstanceEdit.js index 6453fa885..1d7a3e6e7 100644 --- a/src/Instance/InstanceEdit/InstanceEdit.js +++ b/src/Instance/InstanceEdit/InstanceEdit.js @@ -42,14 +42,16 @@ const InstanceEdit = ({ const { instance, isLoading: isInstanceLoading } = useInstance(instanceId, mutator.instanceEdit); const parentInstances = useLoadSubInstances(instance?.parentInstances, 'superInstanceId'); const childInstances = useLoadSubInstances(instance?.childInstances, 'subInstanceId'); + const relatedInstances = useLoadSubInstances(instance?.relatedInstances, 'relatedInstanceId'); useEffect(() => { setInitialValues({ ...unmarshalInstance(instance, identifierTypesById), parentInstances, childInstances, + relatedInstances, }); - }, [instance, identifierTypesById, parentInstances, childInstances]); + }, [instance, identifierTypesById, parentInstances, childInstances, relatedInstances]); const goBack = useGoBack(`/inventory/view/${instanceId}`); @@ -115,7 +117,7 @@ InstanceEdit.manifest = Object.freeze({ InstanceEdit.propTypes = { instanceId: PropTypes.string.isRequired, mutator: PropTypes.object.isRequired, - referenceData: PropTypes.object.isRequired, + referenceData: PropTypes.object, stripes: stripesShape.isRequired, }; diff --git a/src/Instance/InstanceEdit/RelatedInstanceField/RelatedInstanceField.js b/src/Instance/InstanceEdit/RelatedInstanceField/RelatedInstanceField.js new file mode 100644 index 000000000..2e0dfa847 --- /dev/null +++ b/src/Instance/InstanceEdit/RelatedInstanceField/RelatedInstanceField.js @@ -0,0 +1,118 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { FormattedMessage, useIntl } from 'react-intl'; +import { Field } from 'react-final-form'; + +import { + KeyValue, + Row, + Col, + Select, +} from '@folio/stripes/components'; + +import InstancePlugin from '../../../components/InstancePlugin'; +import TitleLabel from '../../../components/TitleLabel'; +import { getIdentifiers, convertInstanceFormatIdsToNames } from '../../../utils'; +import { indentifierTypeNames } from '../../../constants'; +import useReferenceData from '../../../hooks/useReferenceData'; + +const InstanceField = ({ + field, + index, + fields, + isDisabled, + relatedInstanceTypes, +}) => { + const intl = useIntl(); + const { identifierTypesById, instanceFormatsById } = useReferenceData(); + const { update, value } = fields; + const instance = value[index]; + const { + id, + hrid, + title, + identifiers, + instanceFormatIds, + } = instance; + const { + ISSN, + ISBN, + } = indentifierTypeNames; + const issn = getIdentifiers(identifiers, ISSN, identifierTypesById); + const isbn = getIdentifiers(identifiers, ISBN, identifierTypesById); + const format = convertInstanceFormatIdsToNames(instanceFormatIds, instanceFormatsById); + + const handleSelect = (inst) => { + update(index, { + ...inst, + relatedInstanceId: inst.id, + }); + }; + + return ( + + + } + subLabel={id && } + required + /> + } + value={title || } + /> + + + + + + } + value={hrid || } + /> + + + } + value={isbn || } + /> + + + } + value={issn || } + /> + + + } + value={format || } + /> + + + } + name={`${field}.relatedInstanceTypeId`} + dataOptions={relatedInstanceTypes} + placeholder={intl.formatMessage({ id: 'ui-inventory.selectType' })} + required + disabled={isDisabled} + /> + + + ); +}; + +InstanceField.propTypes = { + field: PropTypes.string, + fields: PropTypes.object, + index: PropTypes.number, + isDisabled: PropTypes.bool, + relatedInstanceTypes: PropTypes.arrayOf(PropTypes.object), +}; + +export default InstanceField; diff --git a/src/Instance/InstanceEdit/RelatedInstanceField/index.js b/src/Instance/InstanceEdit/RelatedInstanceField/index.js new file mode 100644 index 000000000..24e169c86 --- /dev/null +++ b/src/Instance/InstanceEdit/RelatedInstanceField/index.js @@ -0,0 +1 @@ +export { default } from './RelatedInstanceField'; diff --git a/src/Instance/InstanceEdit/RelatedInstanceFields/RelatedInstanceFields.js b/src/Instance/InstanceEdit/RelatedInstanceFields/RelatedInstanceFields.js new file mode 100644 index 000000000..d0e042a35 --- /dev/null +++ b/src/Instance/InstanceEdit/RelatedInstanceFields/RelatedInstanceFields.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FieldArray } from 'react-final-form-arrays'; +import { FormattedMessage } from 'react-intl'; + +import { RepeatableField } from '@folio/stripes/components'; + +import RelatedInstanceField from '../RelatedInstanceField'; + +const RelatedInstanceFields = ({ + canAdd, + canEdit, + canDelete, + isDisabled, + relatedInstanceTypes, +}) => ( + } + id="clickable-add-related-instance" + component={RepeatableField} + name="relatedInstances" + canAdd={canAdd} + canRemove={canDelete} + canEdit={canEdit} + renderField={(field, index, fields) => ( + + )} + /> +); + +RelatedInstanceFields.propTypes = { + canAdd: PropTypes.bool, + canEdit: PropTypes.bool, + canDelete: PropTypes.bool, + isDisabled: PropTypes.bool, + relatedInstanceTypes: PropTypes.arrayOf(PropTypes.object), +}; + +RelatedInstanceFields.defaultProps = { + canAdd: true, + canEdit: true, + canDelete: true, + isDisabled: false, +}; + +export default RelatedInstanceFields; diff --git a/src/Instance/InstanceEdit/RelatedInstanceFields/RelatedInstanceFields.test.js b/src/Instance/InstanceEdit/RelatedInstanceFields/RelatedInstanceFields.test.js new file mode 100644 index 000000000..38079cd91 --- /dev/null +++ b/src/Instance/InstanceEdit/RelatedInstanceFields/RelatedInstanceFields.test.js @@ -0,0 +1,92 @@ +import React from 'react'; +import { noop, keyBy } from 'lodash'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { Button } from '@folio/stripes-testing'; + +import '../../../../test/jest/__mock__'; + +import { StripesContext } from '@folio/stripes-core/src/StripesContext'; +import stripesFinalForm from '@folio/stripes/final-form'; + +import { DataContext } from '../../../contexts'; +import { + renderWithIntl, + translationsProperties, + stripesStub, +} from '../../../../test/jest/helpers'; +import { + identifierTypes, + instanceRelationshipTypes, + relatedInstances, + relatedInstanceTypes, +} from '../../../../test/fixtures'; + +import RelatedInstanceFields from './RelatedInstanceFields'; + +export const relatedInstanceOptions = relatedInstanceTypes.map(it => ({ + label: it.name, + value: it.id, +})); + +const Form = stripesFinalForm({})(({ children }) =>
{children}
); + +const RelatedInstanceFieldsSetup = () => ( + + + +
+ + +
+
+
+); + +describe('RelatedInstanceFields', () => { + beforeEach(() => { + renderWithIntl( + , + translationsProperties + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render related instances', () => { + expect(document.querySelectorAll('#clickable-add-related-instance li').length).toEqual(1); + }); + + describe('change related instance', () => { + beforeEach(async () => { + await Button('+').click(); + }); + + it('should change instance', () => { + expect(document.querySelectorAll('[data-test="related-instance-title-0"] .kvValue')[0].innerHTML).toEqual('new instance'); + }); + }); + + describe('add related instance', () => { + beforeEach(async () => { + await Button('Add related instance').click(); + }); + + it('should add related instance', () => { + expect(document.querySelectorAll('#clickable-add-related-instance li').length).toEqual(2); + }); + }); +}); diff --git a/src/Instance/InstanceEdit/RelatedInstanceFields/index.js b/src/Instance/InstanceEdit/RelatedInstanceFields/index.js new file mode 100644 index 000000000..52f569d0b --- /dev/null +++ b/src/Instance/InstanceEdit/RelatedInstanceFields/index.js @@ -0,0 +1 @@ +export { default } from './RelatedInstanceFields'; diff --git a/src/edit/InstanceForm.js b/src/edit/InstanceForm.js index 1794311df..4f177bf10 100644 --- a/src/edit/InstanceForm.js +++ b/src/edit/InstanceForm.js @@ -64,10 +64,12 @@ import { import { validateTitles, validateSubInstances, + validateRelatedInstances, } from '../validation'; import ParentInstanceFields from '../Instance/InstanceEdit/ParentInstanceFields'; import ChildInstanceFields from '../Instance/InstanceEdit/ChildInstanceFields'; +import RelatedInstanceFields from '../Instance/InstanceEdit/RelatedInstanceFields'; import styles from './InstanceForm.css'; @@ -152,6 +154,8 @@ function validate(values) { validateSubInstances(values, 'parentInstances', errors, requiredTextMessage); validateSubInstances(values, 'childInstances', errors, requiredTextMessage); + validateRelatedInstances(values, errors, requiredTextMessage); + return errors; } @@ -276,13 +280,10 @@ class InstanceForm extends React.Component { }), ) : []; - const modeOfIssuanceOptions = referenceTables.modesOfIssuance ? referenceTables.modesOfIssuance.map( - it => ({ - label: it.name, - value: it.id, - selected: it.id === initialValues.modeOfIssuanceId, - }), - ) : []; + const relatedInstanceTypesOptions = referenceTables?.relatedInstanceTypes?.map(it => ({ + label: it.name, + value: it.id, + })) ?? []; const statisticalCodeOptions = referenceTables.statisticalCodes .map( @@ -303,6 +304,14 @@ class InstanceForm extends React.Component { value: it.id, })); + const modeOfIssuanceOptions = referenceTables.modesOfIssuance ? referenceTables.modesOfIssuance.map( + it => ({ + label: it.name, + value: it.id, + selected: it.id === initialValues.modeOfIssuanceId, + }), + ) : []; + const shortcuts = [ { name: 'expandAllSections', @@ -742,6 +751,21 @@ class InstanceForm extends React.Component { /> + + + + )} + id="instanceSection12" + > + + diff --git a/src/providers/DataProvider.js b/src/providers/DataProvider.js index 76468a14e..b68736bea 100644 --- a/src/providers/DataProvider.js +++ b/src/providers/DataProvider.js @@ -43,6 +43,7 @@ const DataProvider = ({ instanceRelationshipTypes, statisticalCodeTypes, statisticalCodes, + instanceFormats, } = loadedData; loadedData.locationsById = keyBy(locations, 'id'); @@ -50,6 +51,8 @@ const DataProvider = ({ loadedData.identifierTypesByName = keyBy(identifierTypes, 'name'); loadedData.holdingsSourcesByName = keyBy(holdingsSources, 'name'); loadedData.instanceRelationshipTypesById = keyBy(instanceRelationshipTypes, 'id'); + loadedData.instanceFormatsById = keyBy(instanceFormats, 'id'); + const statisticalCodeTypesById = keyBy(statisticalCodeTypes, 'id'); // attach full statisticalCodeType object to each statisticalCode @@ -258,6 +261,15 @@ DataProvider.manifest = { }, records: 'holdingsRecordsSources', resourceShouldRefresh: true, + }, + relatedInstanceTypes: { + type: 'okapi', + path: 'related-instance-types', + params: { + limit: '1000', + }, + records: 'relatedInstanceTypes', + resourceShouldRefresh: true, } }; diff --git a/src/utils.js b/src/utils.js index 40bb90172..194c1216f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -747,3 +747,8 @@ export const parseHttpError = async httpError => { return httpError; } }; + + +export const convertInstanceFormatIdsToNames = (instanceFormatIds, instanceFormatsById) => { + return instanceFormatIds?.map(id => instanceFormatsById?.[id]?.name).filter(name => name).join(', '); +}; diff --git a/src/validation.js b/src/validation.js index 84d2c8866..a84272fc0 100644 --- a/src/validation.js +++ b/src/validation.js @@ -30,4 +30,22 @@ export const validateSubInstances = (instance, type, errors, message) => { } }; + +export const validateRelatedInstances = (instance, errors, message) => { + const errorList = []; + + instance.relatedInstances = (instance?.relatedInstances ?? []).forEach((inst, index) => { + const { relatedInstanceTypeId } = inst; + + if (!relatedInstanceTypeId) { + errorList[index] = { relatedInstanceTypeId: message }; + } + }); + + if (errorList.length) { + errors.relatedInstances = errorList; + } +}; + + export default {}; diff --git a/test/fixtures/index.js b/test/fixtures/index.js index d82ca593d..8b14eb984 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -3,6 +3,8 @@ export { instanceRelationshipTypes } from './instanceRelationshipTypes'; export { instances } from './instances'; export { instance } from './instance'; export { subInstances } from './subInstances'; +export { relatedInstances } from './relatedInstances'; export { relationshipTypes } from './relationshipTypes'; +export { relatedInstanceTypes } from './relatedInstanceTypes'; export { childInstances } from './childInstances'; export { items } from './callNumbers'; diff --git a/test/fixtures/relatedInstanceTypes.js b/test/fixtures/relatedInstanceTypes.js new file mode 100644 index 000000000..1778fe98d --- /dev/null +++ b/test/fixtures/relatedInstanceTypes.js @@ -0,0 +1,18 @@ +export const relatedInstanceTypes = [ + { + id: '43d76de7-d317-4fa3-9b15-a2655d38020c', + name: 'Constituent unit', + metadata: { + 'createdDate': '2021-03-22T03:08:10.252+00:00', + 'updatedDate': '2021-03-22T03:08:10.252+00:00' + } + }, + { + id: '3a54132b-14ae-4839-8eb7-4b74889e45f5', + name: 'Data source', + metadata: { + 'createdDate': '2021-03-22T03:08:10.252+00:00', + 'updatedDate': '2021-03-22T03:08:10.252+00:00' + } + } +]; diff --git a/test/fixtures/relatedInstances.js b/test/fixtures/relatedInstances.js new file mode 100644 index 000000000..a3623c301 --- /dev/null +++ b/test/fixtures/relatedInstances.js @@ -0,0 +1,24 @@ +export const relatedInstances = [ + { + 'id': 'e54b1f4d-7d05-4b1a-9368-3c36b75d8ac6', + 'subInstanceId': 'e54b1f4d-7d05-4b1a-9368-3c36b75d8ac6', + 'instanceRelationshipTypeId': 'a17daf0a-f057-43b3-9997-13d0724cdf51', + 'title': 'A semantic web primer', + 'identifiers': [{ + 'identifierTypeId': '8261054f-be78-422d-bd51-4ed9f33c3422', + 'value': '0262012103' + }, { + 'identifierTypeId': '8261054f-be78-422d-bd51-4ed9f33c3422', + 'value': '9780262012102' + }, { + 'identifierTypeId': 'c858e4f2-2b6b-4385-842b-60732ee14abb', + 'value': '2003065165' + }], + 'publication': [{ + 'publisher': 'MIT Press', + 'place': 'Cambridge, Mass. ', + 'dateOfPublication': 'c2004', + 'role': 'Publisher' + }], + } +]; diff --git a/translations/ui-inventory/en.json b/translations/ui-inventory/en.json index e619aabf9..f8f097f1a 100644 --- a/translations/ui-inventory/en.json +++ b/translations/ui-inventory/en.json @@ -328,6 +328,7 @@ "addNewItemDialog": "Add new item dialog", "parentInstances": "Parent instances", "addParentInstance": "Add parent instance", + "addRelatedInstance": "Add related instance", "selectType": "Select type", "selectStatus": "Select status", "loanTypes": "Loan types", From d561d7b4c82e0a1379a0623459885e269a4d55c6 Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Sun, 1 May 2022 21:33:23 -0400 Subject: [PATCH 2/2] Update related instance record --- src/utils.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/utils.js b/src/utils.js index 194c1216f..7ad422406 100644 --- a/src/utils.js +++ b/src/utils.js @@ -598,6 +598,34 @@ export const marshalRelationship = (instance, relationshipName, relationshipIdKe }); }; +/** + * Marshal related instances + * to the format required by the server. + * + * @param instance instance object + * + */ +export const marshalRelatedInstances = (instance) => { + instance.relatedInstances = (instance?.relatedInstances ?? []).map((inst) => { + const { + id, + relatedInstanceTypeId, + } = inst; + + const relationshipRecord = { + instanceId: instance.id, + relatedInstanceId: inst.relatedInstanceId, + relatedInstanceTypeId, + }; + + if (!inst.relatedInstanceId && id) { + relationshipRecord.id = id; + } + + return relationshipRecord; + }); +}; + /** * Marshal given instance to the format required by the server. * @@ -614,6 +642,8 @@ export const marshalInstance = (instance, identifierTypesByName) => { marshalRelationship(marshaledInstance, 'parentInstances', 'superInstanceId'); marshalRelationship(marshaledInstance, 'childInstances', 'subInstanceId'); + marshalRelatedInstances(marshaledInstance); + return marshaledInstance; };