Skip to end of banner
Go to start of banner

Spike: [UIOR-156] Leveraging the existing create item/holding forms from orders/receiving

Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

Version 1 Next »

Related Story

https://issues.folio.org/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).

On the other hand, forms are usually implemented differently - as controlled components, passing to them data loaded from back-end and callbacks with code to call save API.

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).

PoC code:

ui-orders, consumer, include the plugin:

<Pluggable
  aria-haspopup="true"
  instanceId={instanceId}
  locationId={locationId}
  searchLabel={<FormattedMessage id="ui-orders.checkIn.buttons.addItem" />}
  stripes={stripes}
  type="create-item"
  addItem={this.onAddItem}
>
  <span>[no instance-selection plugin]</span>
</Pluggable>

where onAddItem is a callback, accepting created item:


onAddItem = (item = {}) => {
  const { dispatch, change } = this.props;

  if (item.id) {
    dispatch(change('itemId', item.id));
  }
}

ui-plugin-create-item:

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>
    );
  }
}

  • No labels