Skip to content

Commit 5fc9ca5

Browse files
committed
Add basic functionality for create mediated request form
1 parent 249bb75 commit 5fc9ca5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+3169
-62
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* Add caret to `react` peer dependency. Refs UIREQMED-23.
88
* Add Search field for Mediated requests activity page. Refs UIREQMED-3.
99
* Add Filters for Mediated requests actions page. Refs UIREQMED-4.
10+
* Implement `Create mediated request` form with basic functionality. Refs UIREQMED-27.
1011

1112
## 1.0.0
1213
* New app created with stripes-cli. Updated module after created with stripes-cli. Refs UIREQMED-1.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@
4343
"regenerator-runtime": "^0.13.3"
4444
},
4545
"dependencies": {
46-
"prop-types": "^15.6.0"
46+
"prop-types": "^15.6.0",
47+
"query-string": "^5.1.0",
48+
"react-final-form": "^6.5.9"
4749
},
4850
"peerDependencies": {
4951
"@folio/stripes": "^9.0.0",
@@ -57,6 +59,7 @@
5759
"app",
5860
"settings"
5961
],
62+
"queryResource": "query",
6063
"displayName": "ui-requests-mediated.meta.title",
6164
"route": "/requests-mediated",
6265
"icons": [

src/components/MediatedRequestsActivities/MediatedRequestsActivities.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState } from 'react';
2-
2+
import PropTypes from 'prop-types';
33
import { FormattedMessage } from 'react-intl';
44

55
import { AppIcon } from '@folio/stripes/core';
@@ -24,7 +24,7 @@ import {
2424
getMediatedRequestsActivitiesUrl,
2525
} from '../../constants';
2626

27-
const MediatedRequestsActivities = () => {
27+
const MediatedRequestsActivities = ({ settings }) => {
2828
const [filterPaneIsVisible, setFilterPaneIsVisible] = useState(true);
2929

3030
const toggleFilterPane = () => {
@@ -67,7 +67,7 @@ const MediatedRequestsActivities = () => {
6767
value={getMediatedRequestsActivitiesUrl()}
6868
separator
6969
/>
70-
<MediatedRequestsFilters />
70+
<MediatedRequestsFilters settings={settings} />
7171
</Pane>
7272
}
7373
<Pane
@@ -82,4 +82,8 @@ const MediatedRequestsActivities = () => {
8282
);
8383
};
8484

85+
MediatedRequestsActivities.propTypes = {
86+
settings: PropTypes.object.isRequired,
87+
};
88+
8589
export default MediatedRequestsActivities;

src/components/MediatedRequestsActivities/MediatedRequestsActivities.test.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ const labelIds = {
2323
};
2424

2525
describe('MediatedRequestsActivities', () => {
26+
const props = {
27+
settings: {},
28+
};
29+
2630
beforeEach(() => {
27-
render(<MediatedRequestsActivities />);
31+
render(<MediatedRequestsActivities {...props} />);
2832
});
2933

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

5256
it('should trigger MediatedRequestsFilters with correct props', () => {
53-
expect(MediatedRequestsFilters).toHaveBeenCalledWith(expect.objectContaining({}), {});
57+
expect(MediatedRequestsFilters).toHaveBeenCalledWith(expect.objectContaining({
58+
settings: props.settings,
59+
}), {});
5460
});
5561

5662
it('should render CollapseFilterPaneButton', () => {
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
import { Component } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { FormattedMessage } from 'react-intl';
4+
import { Field } from 'react-final-form';
5+
import { isNull } from 'lodash';
6+
7+
import {
8+
Button,
9+
Col,
10+
Icon,
11+
Row,
12+
TextField,
13+
} from '@folio/stripes/components';
14+
import { Pluggable } from '@folio/stripes/core';
15+
16+
import {
17+
BASE_SPINNER_PROPS,
18+
ENTER_EVENT_KEY,
19+
REQUEST_FORM_FIELD_NAMES,
20+
} from '../../../../constants';
21+
import TitleInformation from '../TitleInformation';
22+
import {
23+
isFormEditing,
24+
memoizeValidation,
25+
} from '../../../../utils';
26+
27+
export const INSTANCE_SEGMENT_FOR_PLUGIN = 'instances';
28+
29+
class InstanceInformation extends Component {
30+
static propTypes = {
31+
triggerValidation: PropTypes.func.isRequired,
32+
findInstance: PropTypes.func.isRequired,
33+
getInstanceValidationData: PropTypes.func.isRequired,
34+
submitting: PropTypes.bool.isRequired,
35+
form: PropTypes.object.isRequired,
36+
values: PropTypes.object.isRequired,
37+
onSetSelectedInstance: PropTypes.func.isRequired,
38+
isLoading: PropTypes.bool.isRequired,
39+
instanceId: PropTypes.string.isRequired,
40+
enterButtonClass: PropTypes.string.isRequired,
41+
request: PropTypes.object,
42+
instanceRequestCount: PropTypes.number,
43+
selectedInstance: PropTypes.object,
44+
};
45+
46+
constructor(props) {
47+
super(props);
48+
49+
this.state = {
50+
shouldValidate: false,
51+
isInstanceClicked: false,
52+
isInstanceBlurred: false,
53+
validatedId: null,
54+
};
55+
}
56+
57+
validate = memoizeValidation(async (instanceId) => {
58+
const { getInstanceValidationData } = this.props;
59+
const { shouldValidate } = this.state;
60+
61+
if (!instanceId) {
62+
return <FormattedMessage id="ui-requests-mediated.form.errors.selectInstance" />;
63+
}
64+
65+
if (instanceId && shouldValidate) {
66+
this.setState({ shouldValidate: false });
67+
68+
const instance = await getInstanceValidationData(instanceId);
69+
70+
return !instance
71+
? <FormattedMessage id="ui-requests-mediated.form.errors.instanceDoesNotExist" />
72+
: undefined;
73+
}
74+
75+
return undefined;
76+
});
77+
78+
handleChange = (event) => {
79+
const { form } = this.props;
80+
const {
81+
isInstanceClicked,
82+
isInstanceBlurred,
83+
validatedId,
84+
} = this.state;
85+
const instanceId = event.target.value;
86+
87+
if (isInstanceClicked || isInstanceBlurred) {
88+
this.setState({
89+
isInstanceClicked: false,
90+
isInstanceBlurred: false,
91+
});
92+
}
93+
94+
if (!isNull(validatedId)) {
95+
this.setState({ validatedId: null });
96+
}
97+
98+
form.change(REQUEST_FORM_FIELD_NAMES.INSTANCE_HRID, instanceId);
99+
};
100+
101+
handleBlur = (input) => () => {
102+
const { triggerValidation } = this.props;
103+
const { validatedId } = this.state;
104+
105+
if (input.value && input.value !== validatedId) {
106+
this.setState({
107+
shouldValidate: true,
108+
isInstanceBlurred: true,
109+
validatedId: input.value,
110+
}, () => {
111+
input.onBlur();
112+
triggerValidation();
113+
});
114+
} else if (!input.value) {
115+
input.onBlur();
116+
}
117+
};
118+
119+
handleClick = (eventKey) => {
120+
const {
121+
values,
122+
onSetSelectedInstance,
123+
findInstance,
124+
triggerValidation,
125+
} = this.props;
126+
const instanceId = values.instance?.hrid;
127+
128+
if (instanceId) {
129+
onSetSelectedInstance(null);
130+
this.setState({ isInstanceClicked: true });
131+
findInstance(instanceId);
132+
133+
if (eventKey === ENTER_EVENT_KEY) {
134+
this.setState({ shouldValidate: true }, triggerValidation);
135+
}
136+
}
137+
};
138+
139+
onKeyDown = (e) => {
140+
if (e.key === ENTER_EVENT_KEY && !e.shiftKey) {
141+
e.preventDefault();
142+
this.handleClick(e.key);
143+
}
144+
};
145+
146+
selectInstance = (instanceFromPlugin) => {
147+
return this.props.findInstance(instanceFromPlugin.hrid);
148+
};
149+
150+
render() {
151+
const {
152+
request,
153+
selectedInstance,
154+
submitting,
155+
values,
156+
isLoading,
157+
instanceRequestCount,
158+
instanceId,
159+
enterButtonClass,
160+
} = this.props;
161+
const {
162+
isInstanceClicked,
163+
isInstanceBlurred,
164+
} = this.state;
165+
const isEditForm = isFormEditing(request);
166+
const titleLevelRequestsCount = request?.titleRequestCount || instanceRequestCount;
167+
const isTitleInfoVisible = selectedInstance && !isLoading;
168+
169+
return (
170+
<Row>
171+
<Col xs={12}>
172+
{
173+
!isEditForm &&
174+
<>
175+
<Row>
176+
<Col xs={9}>
177+
<FormattedMessage id="ui-requests-mediated.form.instance.inputPlaceholder">
178+
{placeholder => {
179+
const key = values.keyOfInstanceIdField ?? 0;
180+
181+
return (
182+
<Field
183+
data-testid="instanceHridField"
184+
key={key}
185+
name={REQUEST_FORM_FIELD_NAMES.INSTANCE_HRID}
186+
validate={this.validate(REQUEST_FORM_FIELD_NAMES.INSTANCE_HRID, key)}
187+
validateFields={[]}
188+
>
189+
{({ input, meta }) => {
190+
const selectInstanceError = meta.touched && meta.error;
191+
const instanceDoesntExistError = (isInstanceClicked || isInstanceBlurred) && meta.error;
192+
const error = selectInstanceError || instanceDoesntExistError || null;
193+
194+
return (
195+
<TextField
196+
{...input}
197+
required
198+
placeholder={placeholder}
199+
label={<FormattedMessage id="ui-requests-mediated.form.instance.inputLabel" />}
200+
error={error}
201+
onChange={this.handleChange}
202+
onBlur={this.handleBlur(input)}
203+
onKeyDown={this.onKeyDown}
204+
/>
205+
);
206+
}}
207+
</Field>
208+
);
209+
}}
210+
</FormattedMessage>
211+
</Col>
212+
<Col xs={3}>
213+
<Button
214+
buttonStyle="primary noRadius"
215+
buttonClass={enterButtonClass}
216+
fullWidth
217+
onClick={this.handleClick}
218+
disabled={submitting}
219+
>
220+
<FormattedMessage id="ui-requests-mediated.form.enterButton" />
221+
</Button>
222+
</Col>
223+
</Row>
224+
<Row>
225+
<Col xs={12}>
226+
<Pluggable
227+
searchButtonStyle="link"
228+
type="find-instance"
229+
searchLabel={<FormattedMessage id="ui-requests-mediated.form.instance.lookupLabel" />}
230+
selectInstance={this.selectInstance}
231+
config={{
232+
availableSegments: [{
233+
name: INSTANCE_SEGMENT_FOR_PLUGIN,
234+
}],
235+
}}
236+
/>
237+
</Col>
238+
</Row>
239+
</>
240+
}
241+
{
242+
isLoading && <Icon {...BASE_SPINNER_PROPS} />
243+
}
244+
{
245+
isTitleInfoVisible &&
246+
<TitleInformation
247+
instanceId={request?.instanceId || selectedInstance.id || instanceId}
248+
titleLevelRequestsCount={titleLevelRequestsCount}
249+
title={selectedInstance.title}
250+
contributors={selectedInstance.contributors || selectedInstance.contributorNames}
251+
publications={selectedInstance.publication}
252+
editions={selectedInstance.editions}
253+
identifiers={selectedInstance.identifiers}
254+
/>
255+
}
256+
</Col>
257+
</Row>
258+
);
259+
}
260+
}
261+
262+
export default InstanceInformation;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './InstanceInformation';

0 commit comments

Comments
 (0)