Skip to content

Commit

Permalink
Merge pull request #19 from folio-org/UIREQMED-27
Browse files Browse the repository at this point in the history
  • Loading branch information
artem-blazhko authored Jul 11, 2024
2 parents 038128b + 2e7dbb9 commit 37968c9
Show file tree
Hide file tree
Showing 69 changed files with 6,470 additions and 80 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* Add Search field for Mediated requests activity page. Refs UIREQMED-3.
* Add Filters for Mediated requests actions page. Refs UIREQMED-4.
* Update permission for Mediated requests. Refs UIREQMED-29.
* Implement `Create mediated request` form with basic functionality. Refs UIREQMED-27.

## 1.0.0
* New app created with stripes-cli. Updated module after created with stripes-cli. Refs UIREQMED-1.
29 changes: 26 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@
"regenerator-runtime": "^0.13.3"
},
"dependencies": {
"prop-types": "^15.6.0"
"prop-types": "^15.6.0",
"query-string": "^5.1.0",
"react-final-form": "^6.5.9"
},
"peerDependencies": {
"@folio/stripes": "^9.0.0",
Expand All @@ -57,6 +59,7 @@
"app",
"settings"
],
"queryResource": "query",
"displayName": "ui-requests-mediated.meta.title",
"route": "/requests-mediated",
"icons": [
Expand Down Expand Up @@ -88,15 +91,35 @@
"mod-settings.global.read.circulation",
"mod-settings.entries.collection.get",
"mod-settings.entries.item.get",
"settings.requests-mediated.enabled"
"settings.requests-mediated.enabled",

"circulation.items-by-instance.get",
"circulation.loans.collection.get",
"circulation.requests.collection.get",
"circulation.requests.item.get",
"circulation-storage.loans.collection.get",
"circulation-storage.loans.item.get",
"circulation-storage.requests.collection.get",
"circulation-storage.requests.item.get",
"users.collection.get",
"users.item.get",
"configuration.entries.collection.get"
],
"visible": true
},
{
"permissionName": "ui-requests-mediated.view-create-edit",
"displayName": "Mediated requests: View, create, edit",
"subPermissions": [
"ui-requests-mediated.view"
"ui-requests-mediated.view",

"circulation.requests.item.post",
"circulation.requests.item.put",
"circulation.requests.allowed-service-points.get",
"circulation-storage.requests.item.post",
"circulation-storage.requests.item.put",
"circulation-storage.requests.item.delete",
"circulation-storage.request-preferences.collection.get"
],
"visible": true
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react';

import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';

import { AppIcon } from '@folio/stripes/core';
Expand All @@ -24,7 +24,7 @@ import {
getMediatedRequestsActivitiesUrl,
} from '../../constants';

const MediatedRequestsActivities = () => {
const MediatedRequestsActivities = ({ settings }) => {
const [filterPaneIsVisible, setFilterPaneIsVisible] = useState(true);

const toggleFilterPane = () => {
Expand Down Expand Up @@ -67,7 +67,7 @@ const MediatedRequestsActivities = () => {
value={getMediatedRequestsActivitiesUrl()}
separator
/>
<MediatedRequestsFilters />
<MediatedRequestsFilters settings={settings} />
</Pane>
}
<Pane
Expand All @@ -82,4 +82,8 @@ const MediatedRequestsActivities = () => {
);
};

MediatedRequestsActivities.propTypes = {
settings: PropTypes.object.isRequired,
};

export default MediatedRequestsActivities;
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ const labelIds = {
};

describe('MediatedRequestsActivities', () => {
const props = {
settings: {},
};

beforeEach(() => {
render(<MediatedRequestsActivities />);
render(<MediatedRequestsActivities {...props} />);
});

it('should render search and sort query', () => {
Expand All @@ -50,7 +54,9 @@ describe('MediatedRequestsActivities', () => {
});

it('should trigger MediatedRequestsFilters with correct props', () => {
expect(MediatedRequestsFilters).toHaveBeenCalledWith(expect.objectContaining({}), {});
expect(MediatedRequestsFilters).toHaveBeenCalledWith(expect.objectContaining({
settings: props.settings,
}), {});
});

it('should render CollapseFilterPaneButton', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
import { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Field } from 'react-final-form';
import { isNull } from 'lodash';

import {
Button,
Col,
Icon,
Row,
TextField,
} from '@folio/stripes/components';
import { Pluggable } from '@folio/stripes/core';

import {
BASE_SPINNER_PROPS,
ENTER_EVENT_KEY,
REQUEST_FORM_FIELD_NAMES,
} from '../../../../constants';
import TitleInformation from '../TitleInformation';
import {
isFormEditing,
memoizeValidation,
} from '../../../../utils';

export const INSTANCE_SEGMENT_FOR_PLUGIN = 'instances';

class InstanceInformation extends Component {
static propTypes = {
triggerValidation: PropTypes.func.isRequired,
findInstance: PropTypes.func.isRequired,
getInstanceValidationData: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
form: PropTypes.object.isRequired,
values: PropTypes.object.isRequired,
onSetSelectedInstance: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired,
enterButtonClass: PropTypes.string.isRequired,
request: PropTypes.object,
instanceRequestCount: PropTypes.number,
selectedInstance: PropTypes.object,
};

constructor(props) {
super(props);

this.state = {
shouldValidate: false,
isInstanceClicked: false,
isInstanceBlurred: false,
validatedId: null,
};
}

validate = memoizeValidation(async (instanceId) => {
const { getInstanceValidationData } = this.props;
const { shouldValidate } = this.state;

if (!instanceId) {
return <FormattedMessage id="ui-requests-mediated.form.errors.selectInstance" />;
}

if (instanceId && shouldValidate) {
this.setState({ shouldValidate: false });

const instance = await getInstanceValidationData(instanceId);

return !instance
? <FormattedMessage id="ui-requests-mediated.form.errors.instanceDoesNotExist" />
: undefined;
}

return undefined;
});

handleChange = (event) => {
const { form } = this.props;
const {
isInstanceClicked,
isInstanceBlurred,
validatedId,
} = this.state;
const instanceId = event.target.value;

if (isInstanceClicked || isInstanceBlurred) {
this.setState({
isInstanceClicked: false,
isInstanceBlurred: false,
});
}

if (!isNull(validatedId)) {
this.setState({ validatedId: null });
}

form.change(REQUEST_FORM_FIELD_NAMES.INSTANCE_HRID, instanceId);
};

handleBlur = (input) => () => {
const { triggerValidation } = this.props;
const { validatedId } = this.state;

if (input.value && input.value !== validatedId) {
this.setState({
shouldValidate: true,
isInstanceBlurred: true,
validatedId: input.value,
}, () => {
input.onBlur();
triggerValidation();
});
} else if (!input.value) {
input.onBlur();
}
};

handleClick = (eventKey) => {
const {
values,
onSetSelectedInstance,
findInstance,
triggerValidation,
} = this.props;
const instanceId = values.instance?.hrid;

if (instanceId) {
onSetSelectedInstance(null);
this.setState({ isInstanceClicked: true });
findInstance(instanceId);

if (eventKey === ENTER_EVENT_KEY) {
this.setState({ shouldValidate: true }, triggerValidation);
}
}
};

onKeyDown = (e) => {
if (e.key === ENTER_EVENT_KEY && !e.shiftKey) {
e.preventDefault();
this.handleClick(e.key);
}
};

selectInstance = (instanceFromPlugin) => {
return this.props.findInstance(instanceFromPlugin.hrid);
};

render() {
const {
request,
selectedInstance,
submitting,
values,
isLoading,
instanceRequestCount,
enterButtonClass,
} = this.props;
const {
isInstanceClicked,
isInstanceBlurred,
} = this.state;
const isEditForm = isFormEditing(request);
const titleLevelRequestsCount = request?.titleRequestCount || instanceRequestCount;
const isTitleInfoVisible = selectedInstance && !isLoading;

return (
<Row>
<Col xs={12}>
{
!isEditForm &&
<>
<Row>
<Col xs={9}>
<FormattedMessage id="ui-requests-mediated.form.instance.inputPlaceholder">
{placeholder => {
const key = values.keyOfInstanceIdField ?? 0;

return (
<Field
data-testid="instanceHridField"
key={key}
name={REQUEST_FORM_FIELD_NAMES.INSTANCE_HRID}
validate={this.validate(REQUEST_FORM_FIELD_NAMES.INSTANCE_HRID, key)}
validateFields={[]}
>
{({ input, meta }) => {
const selectInstanceError = meta.touched && meta.error;
const instanceDoesntExistError = (isInstanceClicked || isInstanceBlurred) && meta.error;
const error = selectInstanceError || instanceDoesntExistError || null;

return (
<TextField
{...input}
required
placeholder={placeholder}
label={<FormattedMessage id="ui-requests-mediated.form.instance.inputLabel" />}
error={error}
onChange={this.handleChange}
onBlur={this.handleBlur(input)}
onKeyDown={this.onKeyDown}
/>
);
}}
</Field>
);
}}
</FormattedMessage>
</Col>
<Col xs={3}>
<Button
id="selectInstanceButton"
buttonStyle="primary noRadius"
buttonClass={enterButtonClass}
fullWidth
onClick={this.handleClick}
disabled={submitting}
>
<FormattedMessage id="ui-requests-mediated.form.enterButton" />
</Button>
</Col>
</Row>
<Row>
<Col xs={12}>
<Pluggable
searchButtonStyle="link"
type="find-instance"
searchLabel={<FormattedMessage id="ui-requests-mediated.form.instance.lookupLabel" />}
selectInstance={this.selectInstance}
config={{
availableSegments: [{
name: INSTANCE_SEGMENT_FOR_PLUGIN,
}],
}}
/>
</Col>
</Row>
</>
}
{
isLoading && <Icon {...BASE_SPINNER_PROPS} />
}
{
isTitleInfoVisible &&
<TitleInformation
instanceId={request?.instanceId || selectedInstance.id}
titleLevelRequestsCount={titleLevelRequestsCount}
title={selectedInstance.title}
contributors={selectedInstance.contributors}
publications={selectedInstance.publication}
editions={selectedInstance.editions}
identifiers={selectedInstance.identifiers}
/>
}
</Col>
</Row>
);
}
}

export default InstanceInformation;
Loading

0 comments on commit 37968c9

Please sign in to comment.