Related Story
https://issuesfolio-org.folioatlassian.orgnet/browse/UIOR-156
Findings:
Current ui-plugin implementations mostly used for displaying/selecting entities from list. I think it's enabled because usually in ui-modules list components implemented as Containers - have its manifest, resources, etc. So ui-plugin-find-vendor just renders a Modal with Vendor's list component inside, passing down corresponding callbacks (selectRow and others).
...
I believe that one of valuable attribute is to make as less changes to existing ui-modules code as possible, so I see only one way: ui-plugin-create-item should be Container component, to whom user passes required properties (instanceId, locationId for example), it loads everything required to ItemFrom ui-inventory component from back-end and passes it down, as well as callback with POST API call to save new item. In this case no changes required to ui-inventory, but ui-plugin-create-item contains some business logic to fetch and post data (so dependency on okapi interfaces is introduced).
Demo record of PoC: https://drive.google.com/open?id=1lFyN37hkgrDJ9NOpzEEV7DgfU3xTU1BM
PoC code:
ui-orders, consumer, include the plugin:
...
Code Block | ||||
---|---|---|---|---|
| ||||
export default class CreateItemWrapper extends React.Component { constructor(props) { super(props); this.connectedCreateItemModal = props.stripes.connect(CreateItemModal); } render() { const { searchButtonStyle, searchLabel } = this.props; const props = omitProps(this.props, ['searchButtonStyle', 'searchLabel', 'marginBottom0', 'marginTop0']); return ( <div className={this.getStyle()}> <Button id="clickable-plugin-create-item" key="searchButton" buttonStyle={searchButtonStyle} onClick={this.openModal} tabIndex="-1" > {searchLabel || <Icon icon="search" color="#fff" />} </Button> {this.state.openModal && ( <this.connectedCreateItemModal closeCB={this.closeModal} {...props} /> )} </div> ); } } import React from 'react'; import PropTypes from 'prop-types'; import { get, keyBy } from 'lodash'; import ItemForm from '@folio/inventory/src/edit/items/ItemForm'; import { Modal, omitProps, } from '@folio/stripes/components'; export default class CreateItemModal extends React.Component { static manifest = { identifierTypes: { type: 'okapi', records: 'identifierTypes', path: 'identifier-types?limit=1000&query=cql.allRecords=1 sortby name', }, contributorTypes: { type: 'okapi', records: 'contributorTypes', path: 'contributor-types?limit=400&query=cql.allRecords=1 sortby name', }, contributorNameTypes: { type: 'okapi', records: 'contributorNameTypes', path: 'contributor-name-types?limit=1000&query=cql.allRecords=1 sortby ordering', }, instanceFormats: { type: 'okapi', records: 'instanceFormats', path: 'instance-formats?limit=1000&query=cql.allRecords=1 sortby name', }, instanceTypes: { type: 'okapi', records: 'instanceTypes', path: 'instance-types?limit=1000&query=cql.allRecords=1 sortby name', }, classificationTypes: { type: 'okapi', records: 'classificationTypes', path: 'classification-types?limit=1000&query=cql.allRecords=1 sortby name', }, alternativeTitleTypes: { type: 'okapi', records: 'alternativeTitleTypes', path: 'alternative-title-types?limit=1000&query=cql.allRecords=1 sortby name', }, locations: { type: 'okapi', records: 'locations', path: 'locations?limit=1000&query=cql.allRecords=1 sortby name', }, instanceRelationshipTypes: { type: 'okapi', records: 'instanceRelationshipTypes', path: 'instance-relationship-types?limit=1000&query=cql.allRecords=1 sortby name', }, instanceStatuses: { type: 'okapi', records: 'instanceStatuses', path: 'instance-statuses?limit=1000&query=cql.allRecords=1 sortby name', }, modesOfIssuance: { type: 'okapi', records: 'issuanceModes', path: 'modes-of-issuance?limit=1000&query=cql.allRecords=1 sortby name', }, electronicAccessRelationships: { type: 'okapi', records: 'electronicAccessRelationships', path: 'electronic-access-relationships?limit=1000&query=cql.allRecords=1 sortby name', }, statisticalCodeTypes: { type: 'okapi', records: 'statisticalCodeTypes', path: 'statistical-code-types?limit=1000&query=cql.allRecords=1 sortby name', }, statisticalCodes: { type: 'okapi', records: 'statisticalCodes', path: 'statistical-codes?limit=1000&query=cql.allRecords=1 sortby statisticalCodeTypeId', }, illPolicies: { type: 'okapi', path: 'ill-policies?limit=1000&query=cql.allRecords=1 sortby name', records: 'illPolicies', }, holdingsTypes: { type: 'okapi', path: 'holdings-types?limit=1000&query=cql.allRecords=1 sortby name', records: 'holdingsTypes', }, callNumberTypes: { type: 'okapi', path: 'call-number-types?limit=1000&query=cql.allRecords=1 sortby name', records: 'callNumberTypes', }, holdingsNoteTypes: { type: 'okapi', path: 'holdings-note-types?limit=1000&query=cql.allRecords=1 sortby name', records: 'holdingsNoteTypes', }, materialTypes: { type: 'okapi', path: 'material-types', records: 'mtypes', }, loanTypes: { type: 'okapi', path: 'loan-types', params: { query: 'cql.allRecords=1 sortby name', limit: '40', }, records: 'loantypes', }, instance: { type: 'okapi', path: 'inventory/instances/!{instanceId}', }, items: { type: 'okapi', records: 'items', path: 'inventory/items', fetch: false, }, holdings: { type: 'okapi', records: 'holdingsRecords', path: 'holdings-storage/holdings?query=instanceId==!{instanceId} and permanentLocationId==!{locationId}', }, }; static propTypes = { stripes: PropTypes.shape({ connect: PropTypes.func.isRequired, }).isRequired, closeCB: PropTypes.func.isRequired, locationId: PropTypes.string.isRequired, addItem: PropTypes.func.isRequired, mutator: PropTypes.object, onCloseModal: PropTypes.func, resources: PropTypes.object, } constructor(props) { super(props); this.connectedApp = props.stripes.connect(ItemForm); this.state = { error: null, }; } closeModal = () => { this.props.closeCB(); this.setState({ error: null, }); } onSubmit = (values) => { const { addItem, mutator } = this.props; mutator.items.POST(values) .then((item) => { addItem(item); this.closeModal(); }) .catch(() => this.setState({ error: 'Error on item creation' })); } render() { const { resources } = this.props; const referenceTables = { contributorTypes: get(resources, 'contributorTypes.records', []), contributorNameTypes: get(resources, 'contributorNameTypes.records', []), instanceRelationshipTypes: get(resources, 'instanceRelationshipTypes.records', []), identifierTypes: get(resources, 'identifierTypes.records', []), classificationTypes: get(resources, 'classificationTypes.records', []), instanceTypes: get(resources, 'instanceTypes.records', []), instanceFormats: get(resources, 'instanceFormats.records', []), alternativeTitleTypes: get(resources, 'alternativeTitleTypes.records', []), instanceStatuses: get(resources, 'instanceStatuses.records', []), modesOfIssuance: get(resources, 'modesOfIssuance.records', []), electronicAccessRelationships: get(resources, 'electronicAccessRelationships.records', []), statisticalCodeTypes: get(resources, 'statisticalCodeTypes.records', []), statisticalCodes: get(resources, 'statisticalCodes.records', []), illPolicies: get(resources, 'illPolicies.records', []), holdingsTypes: get(resources, 'holdingsTypes.records', []), callNumberTypes: get(resources, 'callNumberTypes.records', []), holdingsNoteTypes: get(resources, 'holdingsNoteTypes.records', []), locationsById: keyBy(get(resources, 'locations.records', []), 'id'), materialTypes: get(resources, 'materialTypes.records', []), loanTypes: get(resources, 'loanTypes.records', []), }; const holdings = get(resources, 'holdings.records', [{}]); const holdingsRecord = { ...holdings[0], permanentLocationId: this.props.locationId, }; const instance = get(resources, 'instance.records.0', {}); const props = omitProps(this.props, ['resources', 'mutator', 'closeCB', 'locationId', 'instanceId']); const initialValues = { status: { name: 'Available' }, holdingsRecordId: holdingsRecord.id, }; return ( <Modal size="large" open showHeader={false} dismissible > <div> {this.state.error ? <div>{this.state.error}</div> : null} <this.connectedApp {...props} onSubmit={this.onSubmit} initialValues={initialValues} referenceTables={referenceTables} holdingsRecord={holdingsRecord} instance={instance} onCancel={this.closeModal} /> </div> </Modal> ); } } |
...
Summary:
According to slack thread there are plans to leverage edit of various FOLIO entities in a more common way across the whole platform. But since they are mostly long-term I think we can come up with current solution of using plug-in stripes system to reuse ItemForm of ui-inventory module. However, in comments Zak suggested to change my solution - to leverage ItemForm component from ui-inventory. I think we need to choose from these two ways. I'll attach diagrams with both ways as well as Pros and Cons of each.
My way, Pros:
- no changes required in ui-inventory;
- lest time required for implementation, since code is ready.
Cons:
- Tightly coupled with ui-inventory code, so it depends on changes inside it. I think this could be partially solved with tests coverage in ui-plugin repo, so we can see if it's OK to work with future ui-inventory versions on the build (release) stage.
Drawio | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
Zak's suggestion Pros:
- ui-plugin-create-item is independent from ui-inventory and contains all things required to create item (no tight coupling on other ui-module);
Cons:
- changes required to ui-invenotry (to use newly created plugin);
- more time consuming in this case: to implement solution and check that no duplicated API calls to fetch data in Invenotry forms, etc. (may be important to Acquisitions PO);
Drawio | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|