UNPKG

7.95 kBJavaScriptView Raw
1import React, { Component, createRef } from 'react';
2import PropTypes from 'prop-types';
3import qs from 'qs';
4import get from 'lodash.get';
5
6import AvSelect from './AvSelect';
7import AvSelectField from './AvSelectField';
8
9class AvResourceSelect extends Component {
10 constructor(props) {
11 super(props);
12 this.state = {};
13 this.state.previousOptions = [];
14 }
15
16 select = createRef();
17
18 loadOptions = (...args) => {
19 const [inputValue, , additional = {}] = args;
20 let { page } = additional;
21
22 const shouldReturnPreviousOptions =
23 inputValue.length > 0 &&
24 this.props.minCharsToSearch !== undefined &&
25 inputValue.length < this.props.minCharsToSearch;
26
27 if (shouldReturnPreviousOptions) {
28 return {
29 options: this.state.previousOptions,
30 hasMore: false,
31 additional: {
32 page,
33 limit: this.props.itemsPerPage,
34 },
35 };
36 }
37
38 let data;
39 let params;
40 if (this.props.graphqlConfig) {
41 data = {
42 variables: {
43 perPage: this.props.itemsPerPage,
44 filters: {
45 q: encodeURIComponent(inputValue),
46 },
47 },
48 };
49
50 if (args.length !== 3) {
51 if (typeof this.props.parameters === 'function') {
52 data = this.props.parameters(data);
53 } else {
54 data = {
55 ...data,
56 ...this.props.parameters,
57 };
58 }
59 }
60
61 if (this.props.graphqlConfig.query) {
62 data.query = this.props.graphqlConfig.query;
63 }
64 } else {
65 params = {
66 q: encodeURIComponent(inputValue),
67 limit: this.props.itemsPerPage,
68 customerId: this.props.customerId,
69 };
70 }
71
72 if (args.length !== 3) {
73 if (typeof this.props.parameters === 'function') {
74 params = this.props.parameters(params);
75 } else {
76 params = {
77 ...params,
78 ...this.props.parameters,
79 };
80 }
81 }
82
83 if (args.length === 3) {
84 if (this.props.graphqlConfig) {
85 data.variables.page = page;
86 if (typeof this.props.parameters === 'function') {
87 data = this.props.parameters(data);
88 }
89 } else {
90 params.offset = (page - 1) * this.props.itemsPerPage;
91 if (typeof this.props.parameters === 'function') {
92 params = this.props.parameters(params);
93 } else {
94 params = {
95 ...params,
96 ...this.props.parameters,
97 };
98 }
99 }
100 } else {
101 page = 1;
102 }
103
104 let requiredSatisfied =
105 !this.props.requiredParams || this.props.requiredParams.length === 0;
106
107 if (!requiredSatisfied) {
108 if (this.props.graphqlConfig) {
109 requiredSatisfied = this.props.requiredParams.every(param =>
110 get(data, `variables.filters.${param}`)
111 );
112 } else {
113 requiredSatisfied = this.props.requiredParams.every(
114 param => params[param]
115 );
116 }
117 }
118 if (this.props.isDisabled || !requiredSatisfied) {
119 return {
120 options: [],
121 hasMore: false,
122 };
123 }
124 if (this.props.onPageChange) this.props.onPageChange(inputValue, page);
125 let fetch;
126 if (this.props.graphqlConfig || this.props.method === 'POST') {
127 fetch = () =>
128 this.props.resource.post(data || params, {
129 headers: {
130 'Content-Type': 'application/json',
131 },
132 ...this.props.requestConfig,
133 });
134 } else {
135 fetch = () =>
136 this.props.resource.postGet(
137 qs.stringify(params, {
138 encode: false,
139 arrayFormat: 'repeat',
140 indices: false,
141 allowDots: true,
142 }),
143 {
144 headers: {
145 'Content-Type': 'application/x-www-form-urlencoded',
146 },
147 ...this.props.requestConfig,
148 }
149 );
150 }
151 return fetch()
152 .then(resp => {
153 if (!resp || !resp.data) {
154 throw new Error(`API returned an invalid response.`);
155 }
156 const getResult = this.props.getResult || this.props.resource.getResult;
157 let { hasMore } = this.props;
158 if (hasMore === undefined) {
159 if (this.props.graphqlConfig) {
160 hasMore = data =>
161 get(
162 data.data,
163 `${this.props.graphqlConfig.type}Pagination.pageInfo.hasNextPage`,
164 false
165 );
166 } else {
167 hasMore = ({ totalCount, limit, offset }) =>
168 totalCount > offset + limit;
169 }
170 }
171
172 const items =
173 (typeof getResult === 'function'
174 ? getResult.call(this.props.resource, resp.data)
175 : resp.data[getResult]) || this.data;
176
177 hasMore = typeof hasMore === 'function' ? hasMore(resp.data) : hasMore;
178
179 if (!Array.isArray(items)) {
180 throw new TypeError(
181 `Expected data to be an array but got \`${typeof items}\`. Use the \`getData\` prop to specify how to get the data from the API response.`
182 );
183 }
184 this.setState({ previousOptions: items });
185
186 return {
187 options: items,
188 hasMore,
189 additional: {
190 ...additional,
191 page: page + 1,
192 },
193 };
194 })
195 .catch(() => ({ options: [], hasMore: false }));
196 };
197
198 onFocusHandler = (...args) => {
199 if (this.props.onFocus) this.props.onFocus(...args);
200 };
201
202 render() {
203 const Tag = this.props.label ? AvSelectField : AvSelect;
204 const {
205 delay,
206 debounceTimeout = delay,
207 itemsPerPage,
208 watchParams,
209 cacheUniq,
210 additional,
211 waitUntilFocused,
212 ...rest
213 } = this.props;
214 let _cacheUniq = cacheUniq;
215
216 if (_cacheUniq === undefined && watchParams) {
217 const params = {
218 customerId: this.props.customerId,
219 ...this.props.parameters,
220 };
221 _cacheUniq = watchParams.map(watchParam => params[watchParam]).join(',');
222 }
223
224 return (
225 <Tag
226 selectRef={this.select}
227 loadOptions={this.loadOptions}
228 defaultOptions={waitUntilFocused ? [] : true}
229 pagination
230 raw
231 debounceTimeout={debounceTimeout}
232 cacheUniq={_cacheUniq}
233 additional={{
234 page: 1,
235 ...additional,
236 }}
237 {...rest}
238 onFocus={this.onFocusHandler}
239 />
240 );
241 }
242}
243
244const ucFirst = str => str && str.charAt(0).toUpperCase() + str.slice(1);
245
246AvResourceSelect.create = defaults => {
247 const SpecificAvResourceSelect = props => (
248 <AvResourceSelect {...defaults} {...props} />
249 );
250 SpecificAvResourceSelect.displayName = `Av${ucFirst(
251 defaults.resource.defaultConfig.name
252 )}Select`;
253 return SpecificAvResourceSelect;
254};
255
256AvResourceSelect.propTypes = {
257 requestConfig: PropTypes.object,
258 method: PropTypes.string,
259 resource: PropTypes.shape({
260 postGet: PropTypes.func,
261 post: PropTypes.func,
262 getResult: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
263 }).isRequired,
264 getResult: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
265 hasMore: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
266 delay: PropTypes.number,
267 debounceTimeout: PropTypes.number,
268 label: PropTypes.node,
269 customerId: PropTypes.string,
270 parameters: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
271 itemsPerPage: PropTypes.number,
272 onPageChange: PropTypes.func,
273 isDisabled: PropTypes.bool,
274 requiredParams: PropTypes.array,
275 watchParams: PropTypes.array,
276 // eslint-disable-next-line react/forbid-prop-types
277 cacheUniq: PropTypes.any,
278 additional: PropTypes.object,
279 graphqlConfig: PropTypes.shape({
280 type: PropTypes.string,
281 query: PropTypes.string,
282 }),
283 minCharsToSearch: PropTypes.number,
284 onFocus: PropTypes.func,
285 waitUntilFocused: PropTypes.bool,
286};
287
288AvResourceSelect.defaultProps = {
289 delay: 350,
290 itemsPerPage: 50,
291};
292
293export default AvResourceSelect;