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 | ||||
---|---|---|---|---|
| ||||
onAddItem = (item = {}) => { const { dispatch, change } = this.props; if (item.id) { dispatch(change('itemId', item.id)); } } |
ui-plugin-create-item:
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 | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|