mirror of
https://github.com/strapi/strapi.git
synced 2025-10-29 17:04:13 +00:00
Update SelectOne and SelectMany
This commit is contained in:
parent
1157f6ae75
commit
21177a4670
@ -1,267 +1,69 @@
|
||||
/**
|
||||
*
|
||||
* SelectMany
|
||||
*
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
cloneDeep,
|
||||
includes,
|
||||
isArray,
|
||||
isNull,
|
||||
isUndefined,
|
||||
get,
|
||||
findIndex,
|
||||
isEmpty,
|
||||
} from 'lodash';
|
||||
import { get, isEmpty } from 'lodash';
|
||||
|
||||
// Utils.
|
||||
import { request, templateObject } from 'strapi-helper-plugin';
|
||||
import Select from 'react-select';
|
||||
|
||||
// Component.
|
||||
import SortableList from './SortableList';
|
||||
// CSS.
|
||||
import styles from './styles.scss';
|
||||
|
||||
class SelectMany extends React.PureComponent {
|
||||
state = {
|
||||
isLoading: true,
|
||||
options: [],
|
||||
start: 0,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.getOptions('');
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (isEmpty(prevProps.record) && !isEmpty(this.props.record)) {
|
||||
const values = (
|
||||
get(this.props.record, this.props.relation.alias) || []
|
||||
).map(el => el.id || el._id);
|
||||
|
||||
const options = this.state.options.filter(el => {
|
||||
return !values.includes(el.value.id || el.value._id);
|
||||
});
|
||||
|
||||
this.state.options = options;
|
||||
}
|
||||
|
||||
if (prevState.start !== this.state.start) {
|
||||
this.getOptions('');
|
||||
}
|
||||
}
|
||||
|
||||
getOptions = query => {
|
||||
const params = {
|
||||
_limit: 20,
|
||||
_start: this.state.start,
|
||||
source: this.props.relation.plugin || 'content-manager',
|
||||
};
|
||||
|
||||
// Set `query` parameter if necessary
|
||||
if (query) {
|
||||
delete params._limit;
|
||||
delete params._skip;
|
||||
params[`${this.props.relation.displayedAttribute}_contains`] = query;
|
||||
}
|
||||
// Request URL
|
||||
const requestUrl = `/content-manager/explorer/${this.props.relation.model ||
|
||||
this.props.relation.collection}`;
|
||||
|
||||
// Call our request helper (see 'utils/request')
|
||||
return request(requestUrl, {
|
||||
method: 'GET',
|
||||
params,
|
||||
})
|
||||
.then(response => {
|
||||
/* eslint-disable indent */
|
||||
const options = isArray(response)
|
||||
? response.map(item => ({
|
||||
value: item,
|
||||
label: templateObject(
|
||||
{ mainField: this.props.relation.displayedAttribute },
|
||||
item
|
||||
).mainField,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
value: response,
|
||||
label: response[this.props.relation.displayedAttribute],
|
||||
},
|
||||
];
|
||||
/* eslint-enable indent */
|
||||
const newOptions = cloneDeep(this.state.options);
|
||||
options.map(option => {
|
||||
// Don't add the values when searching
|
||||
if (
|
||||
findIndex(newOptions, o => o.value.id === option.value.id) === -1
|
||||
) {
|
||||
return newOptions.push(option);
|
||||
}
|
||||
});
|
||||
|
||||
return this.setState({
|
||||
options: newOptions,
|
||||
isLoading: false,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
strapi.notification.error(
|
||||
'content-manager.notification.error.relationship.fetch'
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
handleInputChange = value => {
|
||||
const clonedOptions = this.state.options;
|
||||
const filteredValues = clonedOptions.filter(data =>
|
||||
includes(data.label, value)
|
||||
);
|
||||
|
||||
if (filteredValues.length === 0) {
|
||||
return this.getOptions(value);
|
||||
}
|
||||
};
|
||||
|
||||
handleChange = value => {
|
||||
// Remove new added value from available option;
|
||||
this.state.options = this.state.options.filter(
|
||||
el =>
|
||||
!((el.value._id || el.value.id) === (value.value.id || value.value._id))
|
||||
);
|
||||
|
||||
this.props.onAddRelationalItem({
|
||||
key: this.props.relation.alias,
|
||||
value: value.value,
|
||||
});
|
||||
};
|
||||
|
||||
handleBottomScroll = () => {
|
||||
this.setState(prevState => {
|
||||
return {
|
||||
start: prevState.start + 1,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
handleRemove = index => {
|
||||
const values = get(this.props.record, this.props.relation.alias);
|
||||
|
||||
// Add removed value from available option;
|
||||
const toAdd = {
|
||||
value: values[index],
|
||||
label: templateObject(
|
||||
{ mainField: this.props.relation.displayedAttribute },
|
||||
values[index]
|
||||
).mainField,
|
||||
};
|
||||
|
||||
this.setState(prevState => ({
|
||||
options: prevState.options.concat([toAdd]),
|
||||
}));
|
||||
|
||||
this.props.onRemoveRelationItem({
|
||||
key: this.props.relation.alias,
|
||||
index,
|
||||
});
|
||||
};
|
||||
|
||||
// Redirect to the edit page
|
||||
handleClick = (item = {}) => {
|
||||
this.props.onRedirect({
|
||||
model: this.props.relation.collection || this.props.relation.model,
|
||||
id: item.value.id || item.value._id,
|
||||
source: this.props.relation.plugin,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const description = this.props.relation.description ? (
|
||||
<p>{this.props.relation.description}</p>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
const value = get(this.props.record, this.props.relation.alias) || [];
|
||||
|
||||
/* eslint-disable jsx-a11y/label-has-for */
|
||||
function SelectMany({
|
||||
addRelation,
|
||||
mainField,
|
||||
name,
|
||||
isLoading,
|
||||
onInputChange,
|
||||
onMenuClose,
|
||||
onMenuScrollToBottom,
|
||||
options,
|
||||
placeholder,
|
||||
value,
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`form-group ${styles.selectMany} ${value.length > 4 &&
|
||||
styles.selectManyUpdate}`}
|
||||
>
|
||||
<label htmlFor={this.props.relation.alias}>
|
||||
{this.props.relation.alias} <span>({value.length})</span>
|
||||
</label>
|
||||
{description}
|
||||
<>
|
||||
<Select
|
||||
className={`${styles.select}`}
|
||||
id={this.props.relation.alias}
|
||||
isLoading={this.state.isLoading}
|
||||
onChange={this.handleChange}
|
||||
onInputChange={this.handleInputChange}
|
||||
onMenuScrollToBottom={this.handleBottomScroll}
|
||||
options={this.state.options}
|
||||
placeholder={
|
||||
<FormattedMessage id="content-manager.containers.Edit.addAnItem" />
|
||||
id={name}
|
||||
filterOption={el => {
|
||||
if (isEmpty(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return value.findIndex(obj => obj.id === el.value.id) === -1;
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
isMulti
|
||||
isSearchable
|
||||
options={options}
|
||||
onChange={addRelation}
|
||||
onInputChange={onInputChange}
|
||||
onMenuClose={onMenuClose}
|
||||
onMenuScrollToBottom={onMenuScrollToBottom}
|
||||
placeholder={placeholder}
|
||||
value={[]}
|
||||
/>
|
||||
<SortableList
|
||||
items={
|
||||
/* eslint-disable indent */
|
||||
isNull(value) || isUndefined(value) || value.size === 0
|
||||
? null
|
||||
: value.map(item => {
|
||||
if (item) {
|
||||
return {
|
||||
value: get(item, 'value') || item,
|
||||
label:
|
||||
get(item, 'label') ||
|
||||
templateObject(
|
||||
{ mainField: this.props.relation.displayedAttribute },
|
||||
item
|
||||
).mainField ||
|
||||
item.id,
|
||||
};
|
||||
}
|
||||
})
|
||||
}
|
||||
/* eslint-enable indent */
|
||||
isDraggingSibling={this.props.isDraggingSibling}
|
||||
keys={this.props.relation.alias}
|
||||
moveAttr={this.props.moveAttr}
|
||||
moveAttrEnd={this.props.moveAttrEnd}
|
||||
name={this.props.relation.alias}
|
||||
onRemove={this.handleRemove}
|
||||
distance={1}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</div>
|
||||
{!isEmpty(value) && (
|
||||
<ul>
|
||||
{value.map(v => (
|
||||
<li key={v.id}>{get(v, [mainField], '')}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
/* eslint-disable jsx-a11y/label-has-for */
|
||||
}
|
||||
}
|
||||
|
||||
SelectMany.defaultProps = {
|
||||
isDraggingSibling: false,
|
||||
moveAttr: () => {},
|
||||
moveAttrEnd: () => {},
|
||||
value: null,
|
||||
};
|
||||
|
||||
SelectMany.propTypes = {
|
||||
isDraggingSibling: PropTypes.bool,
|
||||
moveAttr: PropTypes.func,
|
||||
moveAttrEnd: PropTypes.func,
|
||||
onAddRelationalItem: PropTypes.func.isRequired,
|
||||
onRedirect: PropTypes.func.isRequired,
|
||||
onRemoveRelationItem: PropTypes.func.isRequired,
|
||||
record: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired,
|
||||
relation: PropTypes.object.isRequired,
|
||||
addRelation: PropTypes.func.isRequired,
|
||||
mainField: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onMenuClose: PropTypes.func.isRequired,
|
||||
onMenuScrollToBottom: PropTypes.func.isRequired,
|
||||
options: PropTypes.array.isRequired,
|
||||
placeholder: PropTypes.node.isRequired,
|
||||
value: PropTypes.array,
|
||||
};
|
||||
|
||||
export default SelectMany;
|
||||
|
||||
@ -0,0 +1,267 @@
|
||||
/**
|
||||
*
|
||||
* SelectMany
|
||||
*
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
cloneDeep,
|
||||
includes,
|
||||
isArray,
|
||||
isNull,
|
||||
isUndefined,
|
||||
get,
|
||||
findIndex,
|
||||
isEmpty,
|
||||
} from 'lodash';
|
||||
|
||||
// Utils.
|
||||
import { request, templateObject } from 'strapi-helper-plugin';
|
||||
|
||||
// Component.
|
||||
import SortableList from './SortableList';
|
||||
// CSS.
|
||||
import styles from './styles.scss';
|
||||
|
||||
class SelectMany extends React.PureComponent {
|
||||
state = {
|
||||
isLoading: true,
|
||||
options: [],
|
||||
start: 0,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.getOptions('');
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (isEmpty(prevProps.record) && !isEmpty(this.props.record)) {
|
||||
const values = (
|
||||
get(this.props.record, this.props.relation.alias) || []
|
||||
).map(el => el.id || el._id);
|
||||
|
||||
const options = this.state.options.filter(el => {
|
||||
return !values.includes(el.value.id || el.value._id);
|
||||
});
|
||||
|
||||
this.state.options = options;
|
||||
}
|
||||
|
||||
if (prevState.start !== this.state.start) {
|
||||
this.getOptions('');
|
||||
}
|
||||
}
|
||||
|
||||
getOptions = query => {
|
||||
const params = {
|
||||
_limit: 20,
|
||||
_start: this.state.start,
|
||||
source: this.props.relation.plugin || 'content-manager',
|
||||
};
|
||||
|
||||
// Set `query` parameter if necessary
|
||||
if (query) {
|
||||
delete params._limit;
|
||||
delete params._skip;
|
||||
params[`${this.props.relation.displayedAttribute}_contains`] = query;
|
||||
}
|
||||
// Request URL
|
||||
const requestUrl = `/content-manager/explorer/${this.props.relation.model ||
|
||||
this.props.relation.collection}`;
|
||||
|
||||
// Call our request helper (see 'utils/request')
|
||||
return request(requestUrl, {
|
||||
method: 'GET',
|
||||
params,
|
||||
})
|
||||
.then(response => {
|
||||
/* eslint-disable indent */
|
||||
const options = isArray(response)
|
||||
? response.map(item => ({
|
||||
value: item,
|
||||
label: templateObject(
|
||||
{ mainField: this.props.relation.displayedAttribute },
|
||||
item
|
||||
).mainField,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
value: response,
|
||||
label: response[this.props.relation.displayedAttribute],
|
||||
},
|
||||
];
|
||||
/* eslint-enable indent */
|
||||
const newOptions = cloneDeep(this.state.options);
|
||||
options.map(option => {
|
||||
// Don't add the values when searching
|
||||
if (
|
||||
findIndex(newOptions, o => o.value.id === option.value.id) === -1
|
||||
) {
|
||||
return newOptions.push(option);
|
||||
}
|
||||
});
|
||||
|
||||
return this.setState({
|
||||
options: newOptions,
|
||||
isLoading: false,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
strapi.notification.error(
|
||||
'content-manager.notification.error.relationship.fetch'
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
handleInputChange = value => {
|
||||
const clonedOptions = this.state.options;
|
||||
const filteredValues = clonedOptions.filter(data =>
|
||||
includes(data.label, value)
|
||||
);
|
||||
|
||||
if (filteredValues.length === 0) {
|
||||
return this.getOptions(value);
|
||||
}
|
||||
};
|
||||
|
||||
handleChange = value => {
|
||||
// Remove new added value from available option;
|
||||
this.state.options = this.state.options.filter(
|
||||
el =>
|
||||
!((el.value._id || el.value.id) === (value.value.id || value.value._id))
|
||||
);
|
||||
|
||||
this.props.onAddRelationalItem({
|
||||
key: this.props.relation.alias,
|
||||
value: value.value,
|
||||
});
|
||||
};
|
||||
|
||||
handleBottomScroll = () => {
|
||||
this.setState(prevState => {
|
||||
return {
|
||||
start: prevState.start + 1,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
handleRemove = index => {
|
||||
const values = get(this.props.record, this.props.relation.alias);
|
||||
|
||||
// Add removed value from available option;
|
||||
const toAdd = {
|
||||
value: values[index],
|
||||
label: templateObject(
|
||||
{ mainField: this.props.relation.displayedAttribute },
|
||||
values[index]
|
||||
).mainField,
|
||||
};
|
||||
|
||||
this.setState(prevState => ({
|
||||
options: prevState.options.concat([toAdd]),
|
||||
}));
|
||||
|
||||
this.props.onRemoveRelationItem({
|
||||
key: this.props.relation.alias,
|
||||
index,
|
||||
});
|
||||
};
|
||||
|
||||
// Redirect to the edit page
|
||||
handleClick = (item = {}) => {
|
||||
this.props.onRedirect({
|
||||
model: this.props.relation.collection || this.props.relation.model,
|
||||
id: item.value.id || item.value._id,
|
||||
source: this.props.relation.plugin,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const description = this.props.relation.description ? (
|
||||
<p>{this.props.relation.description}</p>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
const value = get(this.props.record, this.props.relation.alias) || [];
|
||||
|
||||
/* eslint-disable jsx-a11y/label-has-for */
|
||||
return (
|
||||
<div
|
||||
className={`form-group ${styles.selectMany} ${value.length > 4 &&
|
||||
styles.selectManyUpdate}`}
|
||||
>
|
||||
<label htmlFor={this.props.relation.alias}>
|
||||
{this.props.relation.alias} <span>({value.length})</span>
|
||||
</label>
|
||||
{description}
|
||||
<Select
|
||||
className={`${styles.select}`}
|
||||
id={this.props.relation.alias}
|
||||
isLoading={this.state.isLoading}
|
||||
onChange={this.handleChange}
|
||||
onInputChange={this.handleInputChange}
|
||||
onMenuScrollToBottom={this.handleBottomScroll}
|
||||
options={this.state.options}
|
||||
placeholder={
|
||||
<FormattedMessage id="content-manager.containers.Edit.addAnItem" />
|
||||
}
|
||||
/>
|
||||
<SortableList
|
||||
items={
|
||||
/* eslint-disable indent */
|
||||
isNull(value) || isUndefined(value) || value.size === 0
|
||||
? null
|
||||
: value.map(item => {
|
||||
if (item) {
|
||||
return {
|
||||
value: get(item, 'value') || item,
|
||||
label:
|
||||
get(item, 'label') ||
|
||||
templateObject(
|
||||
{ mainField: this.props.relation.displayedAttribute },
|
||||
item
|
||||
).mainField ||
|
||||
item.id,
|
||||
};
|
||||
}
|
||||
})
|
||||
}
|
||||
/* eslint-enable indent */
|
||||
isDraggingSibling={this.props.isDraggingSibling}
|
||||
keys={this.props.relation.alias}
|
||||
moveAttr={this.props.moveAttr}
|
||||
moveAttrEnd={this.props.moveAttrEnd}
|
||||
name={this.props.relation.alias}
|
||||
onRemove={this.handleRemove}
|
||||
distance={1}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
/* eslint-disable jsx-a11y/label-has-for */
|
||||
}
|
||||
}
|
||||
|
||||
SelectMany.defaultProps = {
|
||||
isDraggingSibling: false,
|
||||
moveAttr: () => {},
|
||||
moveAttrEnd: () => {},
|
||||
};
|
||||
|
||||
SelectMany.propTypes = {
|
||||
isDraggingSibling: PropTypes.bool,
|
||||
moveAttr: PropTypes.func,
|
||||
moveAttrEnd: PropTypes.func,
|
||||
onAddRelationalItem: PropTypes.func.isRequired,
|
||||
onRedirect: PropTypes.func.isRequired,
|
||||
onRemoveRelationItem: PropTypes.func.isRequired,
|
||||
record: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired,
|
||||
relation: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default SelectMany;
|
||||
@ -1,135 +1,53 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { get, isEmpty, isNull } from 'lodash';
|
||||
import { request } from 'strapi-helper-plugin';
|
||||
import { get, isNull } from 'lodash';
|
||||
|
||||
import Select from 'react-select';
|
||||
|
||||
import pluginId from '../../pluginId';
|
||||
|
||||
import { Nav, Wrapper } from './components';
|
||||
|
||||
function SelectOne({
|
||||
description,
|
||||
label,
|
||||
mainField,
|
||||
name,
|
||||
isLoading,
|
||||
onChange,
|
||||
pathname,
|
||||
search,
|
||||
targetModel,
|
||||
plugin,
|
||||
onInputChange,
|
||||
onMenuClose,
|
||||
onMenuScrollToBottom,
|
||||
options,
|
||||
placeholder,
|
||||
value,
|
||||
}) {
|
||||
const [state, setState] = useState({
|
||||
_q: '',
|
||||
_limit: 8,
|
||||
_start: 0,
|
||||
source: isEmpty(plugin) ? 'content-manager' : plugin,
|
||||
});
|
||||
const [options, setOptions] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const ref = useRef();
|
||||
|
||||
ref.current = async (uid, params = state) => {
|
||||
try {
|
||||
const requestUrl = `/${pluginId}/explorer/${uid}`;
|
||||
|
||||
if (isEmpty(params._q)) {
|
||||
delete params._q;
|
||||
}
|
||||
|
||||
const data = await request(requestUrl, {
|
||||
method: 'GET',
|
||||
params: params,
|
||||
});
|
||||
|
||||
setOptions(
|
||||
data.map(obj => {
|
||||
return { value: obj, label: obj[mainField] };
|
||||
})
|
||||
);
|
||||
setIsLoading(false);
|
||||
} catch (err) {
|
||||
console.log({ err });
|
||||
|
||||
strapi.notification.error('notification.error');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
ref.current(targetModel);
|
||||
}, [ref, targetModel, state._start]);
|
||||
|
||||
const nextSearch = `${pathname}${search}`;
|
||||
const to = `/plugins/${pluginId}/${targetModel}/${
|
||||
value ? value.id : null
|
||||
}?redirectUrl=${nextSearch}`;
|
||||
const link =
|
||||
value === null ||
|
||||
value === undefined ||
|
||||
['role', 'permission'].includes(targetModel) ? null : (
|
||||
<Link to={to}>
|
||||
<FormattedMessage id="content-manager.containers.Edit.seeDetails" />
|
||||
</Link>
|
||||
);
|
||||
|
||||
const onInputChange = inputValue => {
|
||||
setState(prevState => ({ ...prevState, _q: inputValue }));
|
||||
|
||||
return inputValue;
|
||||
};
|
||||
|
||||
const onMenuScrollToBottom = () => {
|
||||
setState(prevState => ({ ...prevState, _start: prevState._start + 1 }));
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper className="form-group">
|
||||
<Nav>
|
||||
<label htmlFor={name}>{label}</label>
|
||||
{link}
|
||||
</Nav>
|
||||
{!isEmpty(description) && <p>{description}</p>}
|
||||
<Select
|
||||
id={name}
|
||||
isLoading={isLoading}
|
||||
isClearable
|
||||
options={options}
|
||||
onChange={value => {
|
||||
onChange({ target: { name, value: value ? value.value : value } });
|
||||
}}
|
||||
onChange={onChange}
|
||||
onInputChange={onInputChange}
|
||||
onMenuClose={() => {
|
||||
setState(prevState => ({ ...prevState, _start: 0 }));
|
||||
}}
|
||||
onMenuClose={onMenuClose}
|
||||
onMenuScrollToBottom={onMenuScrollToBottom}
|
||||
placeholder={placeholder}
|
||||
value={
|
||||
isNull(value) ? null : { label: get(value, [mainField], ''), value }
|
||||
}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
SelectOne.defaultProps = {
|
||||
description: '',
|
||||
label: '',
|
||||
plugin: '',
|
||||
value: null,
|
||||
};
|
||||
|
||||
SelectOne.propTypes = {
|
||||
description: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
mainField: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
pathname: PropTypes.string.isRequired,
|
||||
plugin: PropTypes.string,
|
||||
search: PropTypes.string.isRequired,
|
||||
targetModel: PropTypes.string.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onMenuClose: PropTypes.func.isRequired,
|
||||
onMenuScrollToBottom: PropTypes.func.isRequired,
|
||||
options: PropTypes.array.isRequired,
|
||||
placeholder: PropTypes.node.isRequired,
|
||||
value: PropTypes.object,
|
||||
};
|
||||
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
|
||||
label {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 500;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
nav + div {
|
||||
height: 34px;
|
||||
margin: 3px 0 26px;
|
||||
|
||||
> div {
|
||||
box-shadow: none !important;
|
||||
border-color: #e3e9f3 !important;
|
||||
|
||||
> span:first-of-type {
|
||||
> div:first-of-type {
|
||||
color: #9ea7b8;
|
||||
}
|
||||
}
|
||||
|
||||
> span:last-of-type {
|
||||
span {
|
||||
border-color: #b3b5b9 transparent transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Nav = styled.nav`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
a {
|
||||
color: #007eff !important;
|
||||
font-size: 1.3rem;
|
||||
padding-top: 3px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export { Nav, Wrapper };
|
||||
@ -0,0 +1,180 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { request } from 'strapi-helper-plugin';
|
||||
|
||||
import pluginId from '../../pluginId';
|
||||
|
||||
import SelectOne from '../SelectOne';
|
||||
import SelectMany from '../SelectMany';
|
||||
|
||||
import { Nav, Wrapper } from './components';
|
||||
|
||||
function SelectWrapper({
|
||||
addRelation,
|
||||
label,
|
||||
mainField,
|
||||
name,
|
||||
onChange,
|
||||
pathname,
|
||||
relationType,
|
||||
search,
|
||||
targetModel,
|
||||
plugin,
|
||||
value,
|
||||
}) {
|
||||
const [state, setState] = useState({
|
||||
_q: '',
|
||||
_limit: 8,
|
||||
_start: 0,
|
||||
source: isEmpty(plugin) ? 'content-manager' : plugin,
|
||||
});
|
||||
const [options, setOptions] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const ref = useRef();
|
||||
const startRef = useRef();
|
||||
startRef.current = state._start;
|
||||
|
||||
ref.current = async (uid, params = state) => {
|
||||
try {
|
||||
const requestUrl = `/${pluginId}/explorer/${uid}`;
|
||||
|
||||
if (isEmpty(params._q)) {
|
||||
delete params._q;
|
||||
}
|
||||
|
||||
const data = await request(requestUrl, {
|
||||
method: 'GET',
|
||||
params: params,
|
||||
});
|
||||
const formattedData = data.map(obj => {
|
||||
return { value: obj, label: obj[mainField] };
|
||||
});
|
||||
|
||||
if (!isEmpty(params._q)) {
|
||||
setOptions(formattedData);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setOptions(prevState =>
|
||||
prevState.concat(formattedData).filter((obj, index) => {
|
||||
const objIndex = prevState.findIndex(
|
||||
el => el.value.id === obj.value.id
|
||||
);
|
||||
|
||||
if (objIndex === -1) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
prevState.findIndex(el => el.value.id === obj.value.id) === index
|
||||
);
|
||||
})
|
||||
);
|
||||
setIsLoading(false);
|
||||
} catch (err) {
|
||||
strapi.notification.error('notification.error');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
ref.current(targetModel);
|
||||
|
||||
return () => {};
|
||||
}, [ref, targetModel, state._start, state._q]);
|
||||
|
||||
const onInputChange = inputValue => {
|
||||
setState(prevState => {
|
||||
if (prevState._q === inputValue) {
|
||||
return prevState;
|
||||
}
|
||||
|
||||
return { ...prevState, _q: inputValue };
|
||||
});
|
||||
|
||||
return inputValue;
|
||||
};
|
||||
|
||||
const onMenuScrollToBottom = () => {
|
||||
setState(prevState => ({ ...prevState, _start: prevState._start + 1 }));
|
||||
};
|
||||
const isSingle = [
|
||||
'oneWay',
|
||||
'oneToOne',
|
||||
'manyToOne',
|
||||
'oneToManyMorph',
|
||||
'oneToOneMorph',
|
||||
].includes(relationType);
|
||||
const nextSearch = `${pathname}${search}`;
|
||||
const to = `/plugins/${pluginId}/${targetModel}/${
|
||||
value ? value.id : null
|
||||
}?redirectUrl=${nextSearch}`;
|
||||
const link =
|
||||
value === null ||
|
||||
value === undefined ||
|
||||
['role', 'permission'].includes(targetModel) ? null : (
|
||||
<Link to={to}>
|
||||
<FormattedMessage id="content-manager.containers.Edit.seeDetails" />
|
||||
</Link>
|
||||
);
|
||||
const Component = isSingle ? SelectOne : SelectMany;
|
||||
|
||||
return (
|
||||
<Wrapper className="form-group">
|
||||
<Nav>
|
||||
<label htmlFor={name}>{label}</label>
|
||||
{isSingle && link}
|
||||
</Nav>
|
||||
<Component
|
||||
addRelation={value => {
|
||||
addRelation({ target: { name, value } });
|
||||
}}
|
||||
id={name}
|
||||
isLoading={isLoading}
|
||||
isClearable
|
||||
mainField={mainField}
|
||||
name={name}
|
||||
options={options}
|
||||
onChange={value => {
|
||||
onChange({ target: { name, value: value ? value.value : value } });
|
||||
}}
|
||||
onInputChange={onInputChange}
|
||||
onMenuClose={() => {
|
||||
setState(prevState => ({ ...prevState, _q: '', _start: 0 }));
|
||||
}}
|
||||
onMenuScrollToBottom={onMenuScrollToBottom}
|
||||
placeholder={
|
||||
<FormattedMessage id={`${pluginId}.containers.Edit.addAnItem`} />
|
||||
}
|
||||
value={value}
|
||||
/>
|
||||
<div style={{ marginBottom: 26 }} />
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
SelectWrapper.defaultProps = {
|
||||
description: '',
|
||||
label: '',
|
||||
plugin: '',
|
||||
value: null,
|
||||
};
|
||||
|
||||
SelectWrapper.propTypes = {
|
||||
addRelation: PropTypes.func.isRequired,
|
||||
description: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
mainField: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
pathname: PropTypes.string.isRequired,
|
||||
plugin: PropTypes.string,
|
||||
relationType: PropTypes.string.isRequired,
|
||||
search: PropTypes.string.isRequired,
|
||||
targetModel: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
|
||||
};
|
||||
|
||||
export default SelectWrapper;
|
||||
@ -39,7 +39,7 @@ const getInputType = (type = '') => {
|
||||
}
|
||||
};
|
||||
|
||||
function Inputs({ keys, layout, modifiedData, name, onChange }) {
|
||||
function Inputs({ autoFocus, keys, layout, modifiedData, name, onChange }) {
|
||||
const attribute = get(layout, ['schema', 'attributes', name], {});
|
||||
const { model, collection } = attribute;
|
||||
const isMedia =
|
||||
@ -68,6 +68,7 @@ function Inputs({ keys, layout, modifiedData, name, onChange }) {
|
||||
return (
|
||||
<InputsIndex
|
||||
{...metadata}
|
||||
autoFocus={autoFocus}
|
||||
inputDescription={description}
|
||||
inputStyle={inputStyle}
|
||||
customInputs={{ json: InputJSONWithErrors, wysiwyg: WysiwygWithErrors }}
|
||||
@ -82,7 +83,12 @@ function Inputs({ keys, layout, modifiedData, name, onChange }) {
|
||||
);
|
||||
}
|
||||
|
||||
Inputs.defaultProps = {
|
||||
autoFocus: false,
|
||||
};
|
||||
|
||||
Inputs.propTypes = {
|
||||
autoFocus: PropTypes.bool,
|
||||
keys: PropTypes.string.isRequired,
|
||||
layout: PropTypes.object.isRequired,
|
||||
modifiedData: PropTypes.object.isRequired,
|
||||
|
||||
@ -15,7 +15,6 @@ import {
|
||||
import pluginId from '../../pluginId';
|
||||
|
||||
import Container from '../../components/Container';
|
||||
import SelectOne from '../../components/SelectOne';
|
||||
|
||||
import { LinkWrapper, MainWrapper, SubWrapper } from './components';
|
||||
import Group from './Group';
|
||||
@ -23,6 +22,7 @@ import Inputs from './Inputs';
|
||||
|
||||
import init, { setDefaultForm } from './init';
|
||||
import reducer, { initialState } from './reducer';
|
||||
import SelectWrapper from '../../components/SelectWrapper';
|
||||
|
||||
const getRequestUrl = path => `/${pluginId}/explorer/${path}`;
|
||||
|
||||
@ -371,9 +371,10 @@ function EditView({
|
||||
|
||||
return (
|
||||
<div key={key} className="row">
|
||||
{fieldsRow.map(({ name }) => {
|
||||
{fieldsRow.map(({ name }, index) => {
|
||||
return (
|
||||
<Inputs
|
||||
autoFocus={key === 0 && index === 0}
|
||||
key={name}
|
||||
keys={name}
|
||||
layout={layout}
|
||||
@ -413,22 +414,19 @@ function EditView({
|
||||
{}
|
||||
);
|
||||
const value = get(modifiedData, [relationName], null);
|
||||
const Component = [
|
||||
'oneWay',
|
||||
'oneToOne',
|
||||
'manyToOne',
|
||||
'oneToManyMorph',
|
||||
'oneToOneMorph',
|
||||
].includes(relation.relationType)
|
||||
? SelectOne
|
||||
: // eslint-disable-next-line react/display-name
|
||||
() => <div>SelectMany</div>;
|
||||
|
||||
return (
|
||||
<Component
|
||||
key={relationName}
|
||||
<SelectWrapper
|
||||
{...relation}
|
||||
{...relationMetas}
|
||||
key={relationName}
|
||||
addRelation={({ target: { name, value } }) => {
|
||||
dispatch({
|
||||
type: 'ADD_RELATION',
|
||||
keys: name.split('.'),
|
||||
value,
|
||||
});
|
||||
}}
|
||||
name={relationName}
|
||||
onChange={({ target: { name, value } }) => {
|
||||
dispatch({
|
||||
@ -438,6 +436,7 @@ function EditView({
|
||||
});
|
||||
}}
|
||||
pathname={pathname}
|
||||
relationType={relation.relationType}
|
||||
search={search}
|
||||
value={value}
|
||||
/>
|
||||
|
||||
@ -44,6 +44,20 @@ function reducer(state, action) {
|
||||
: { _temp__id: 0 },
|
||||
]);
|
||||
});
|
||||
case 'ADD_RELATION':
|
||||
return state.updateIn(['modifiedData', ...action.keys], list => {
|
||||
if (!action.value) {
|
||||
return list;
|
||||
}
|
||||
|
||||
const el = action.value[0].value;
|
||||
|
||||
if (list) {
|
||||
return list.push(fromJS(el));
|
||||
}
|
||||
|
||||
return fromJS([el]);
|
||||
});
|
||||
case 'GET_DATA_SUCCEEDED':
|
||||
return state
|
||||
.update('initialData', () => fromJS(action.data))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user