@plone/volto
Version:
Volto
638 lines (619 loc) • 21.6 kB
JSX
/**
* QuerystringWidget component.
* @module components/manage/Widgets/QuerystringWidget
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { Button, Form, Grid, Input, Label } from 'semantic-ui-react';
import filter from 'lodash/filter';
import remove from 'lodash/remove';
import toPairs from 'lodash/toPairs';
import groupBy from 'lodash/groupBy';
import isEmpty from 'lodash/isEmpty';
import map from 'lodash/map';
import { defineMessages, injectIntl } from 'react-intl';
import { withRouter } from 'react-router';
import { getQuerystring } from '@plone/volto/actions/querystring/querystring';
import { getQueryStringResults } from '@plone/volto/actions/querystringsearch/querystringsearch';
import Icon from '@plone/volto/components/theme/Icon/Icon';
import ObjectBrowserWidget from '@plone/volto/components/manage/Widgets/ObjectBrowserWidget';
import NumberWidget from '@plone/volto/components/manage/Widgets/NumberWidget';
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
import cx from 'classnames';
import config from '@plone/volto/registry';
import {
Option,
DropdownIndicator,
selectTheme,
customSelectStyles,
} from '@plone/volto/components/manage/Widgets/SelectStyling';
import clearSVG from '@plone/volto/icons/clear.svg';
const messages = defineMessages({
Criteria: {
id: 'Criteria',
defaultMessage: 'Criteria',
},
AddCriteria: {
id: 'Add criteria',
defaultMessage: 'Add criteria',
},
select: {
id: 'querystring-widget-select',
defaultMessage: 'Select…',
},
currentPath: {
id: 'query-widget-currentPath',
defaultMessage: 'Current path (./)',
},
parentPath: {
id: 'query-widget-parentPath',
defaultMessage: 'Parent path (../)',
},
});
const parseUidDepth = (val) => {
if (typeof val !== 'string') return { uid: '', depth: 1 };
const lastSep = String(val).lastIndexOf('::');
if (lastSep !== -1) {
const uid = val.substring(0, lastSep);
const parsed = parseInt(val.substring(lastSep + 2), 10);
return { uid, depth: Number.isNaN(parsed) ? 1 : parsed };
}
return { uid: val, depth: 1 };
};
/**
* Widget for a querystring value, to define a catalog search criteria.
*/
export class QuerystringWidgetComponent extends Component {
/**
* Property types.
* @property {Object} propTypes Property types.
* @static
*/
static propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string,
required: PropTypes.bool,
error: PropTypes.arrayOf(PropTypes.string),
value: PropTypes.array,
focus: PropTypes.bool,
onChange: PropTypes.func,
onEdit: PropTypes.func,
onDelete: PropTypes.func,
getQuerystring: PropTypes.func.isRequired,
};
/**
* Default properties.
* @property {Object} defaultProps Default properties.
* @static
*/
static defaultProps = {
description: null,
required: false,
error: [],
value: null,
onChange: null,
onEdit: null,
onDelete: null,
focus: false,
};
/**
* Constructor
* @method constructor
* @param {Object} props Component properties
* @constructs EditComponent
*/
constructor(props) {
super(props);
this.state = {
visual: false,
};
this.onChangeValue = this.onChangeValue.bind(this);
this.getWidget = this.getWidget.bind(this);
this.loadReferenceWidgetItem = this.loadReferenceWidgetItem.bind(this);
}
/**
* Component did mount lifecycle method
* @method componentDidMount
* @returns {undefined}
*/
componentDidMount() {
if (this.props.focus) {
this.node.focus();
}
this.props.getQuerystring();
}
loadReferenceWidgetItem(v) {
const loading =
this.props.reference[`${v}_query_reference`]?.loading ?? false;
if (!loading && v?.length > 0) {
this.props.getQueryStringResults(
'/',
{
b_size: 1,
query: [
{
i: 'path',
o: 'plone.app.querystring.operation.string.absolutePath',
v: v + '::0',
},
],
},
v + '_query_reference',
);
}
}
/**
* Get correct widget
* @method getWidget
* @param {Object} row Row object.
* @param {number} index Row index.
* @returns {Object} Widget.
*/
getWidget(row, index, Select, intl) {
const props = {
fluid: true,
value: row.v,
onChange: (data) => this.onChangeValue(index, data.target.value),
};
const values = this.props.indexes[row.i].values;
const operator = this.props.indexes[row.i].operators[row.o];
switch (operator.widget) {
case null:
return <span />;
case 'DateWidget':
return (
<Form.Field style={{ flex: '1 0 auto' }}>
<Input type="date" {...props} value={row.v} />
</Form.Field>
);
case 'DateRangeWidget': // 2 date inputs
return (
<React.Fragment>
<Form.Field style={{ flex: '1 0 auto' }}>
<Input
type="date"
{...props}
value={row.v[0]}
onChange={(data) =>
this.onChangeValue(index, [data.target.value, row.v[1]])
}
/>
</Form.Field>
<Form.Field style={{ flex: '1 0 auto' }}>
<Input
type="date"
{...props}
value={row.v[1]}
onChange={(data) =>
this.onChangeValue(index, [row.v[0], data.target.value])
}
/>
</Form.Field>
</React.Fragment>
);
case 'RelativeDateWidget':
return (
<Form.Field style={{ flex: '1 0 auto' }}>
<Input step={1} type="number" {...props} />
</Form.Field>
);
case 'MultipleSelectionWidget':
return (
<Form.Field style={{ flex: '1 0 auto', maxWidth: '92%' }}>
<Select
{...props}
className="react-select-container"
classNamePrefix="react-select"
options={
values
? map(toPairs(values), (value) => ({
label: value[1].title,
value: value[0],
}))
: []
}
styles={customSelectStyles}
placeholder={this.props.intl.formatMessage(messages.select)}
theme={selectTheme}
components={{ DropdownIndicator, Option }}
onChange={(data) => {
this.onChangeValue(
index,
map(data, (item) => item.value),
);
}}
isMulti={true}
value={map(row.v, (value) => ({
label: values?.[value]?.title || value,
value,
}))}
/>
</Form.Field>
);
case 'autocomplete':
const AutoCompleteComponent = config.widgets.widget.autocomplete;
const vocabulary = { '@id': this.props.indexes[row.i].vocabulary };
return (
<Form.Field style={{ flex: '1 0 auto', maxWidth: '92%' }}>
<AutoCompleteComponent
{...props}
vocabulary={vocabulary}
wrapped={false}
id={`id-${index}`}
title={`title-${index}`}
onChange={(_d, data) => {
this.onChangeValue(index, data);
}}
/>
</Form.Field>
);
case 'ReferenceWidget':
const { uid: uidValue, depth: depthValue } = parseUidDepth(props.value);
if (!this.props.reference[`${uidValue}_query_reference`]) {
this.loadReferenceWidgetItem(uidValue);
}
const referenceItem = this.props.reference[
`${uidValue}_query_reference`
]
? this.props.reference[`${uidValue}_query_reference`].items[0]
: null;
return (
<div className="location-object-browser">
<Form.Field className="object-browser-field">
<ObjectBrowserWidget
id={`query-reference-widget-${index}`}
mode="link"
onChange={(id, data) => {
const itemSelected = data.length > 0 ? data[0] : {};
const uid = itemSelected.UID ?? '';
this.onChangeValue(index, uid ? `${uid}::${depthValue}` : '');
this.loadReferenceWidgetItem(uid);
}}
value={uidValue && this.props.reference ? [referenceItem] : []}
wrapped={false}
onlyFolderishSelectable={true}
allowExternals={true}
/>
</Form.Field>
{uidValue && (
<Form.Field className="reference-widget-depth">
<NumberWidget
title={intl.formatMessage({
id: 'Depth',
defaultMessage: 'Depth',
})}
min={1}
step={1}
value={depthValue}
onChange={(id, value) => {
const newDepth = parseInt(value, 10) || 1;
const curUid = uidValue || '';
this.onChangeValue(index, `${curUid}::${newDepth}`);
}}
/>
</Form.Field>
)}
</div>
);
case 'RelativePathWidget':
const relativePathOptions = [
{
label: intl.formatMessage(messages.currentPath),
value: './',
},
{
label: intl.formatMessage(messages.parentPath),
value: '../',
},
];
return (
<Form.Field style={{ flex: '1 0 auto', maxWidth: '92%' }}>
<Select
{...props}
className="react-select-container"
classNamePrefix="react-select"
options={relativePathOptions}
styles={customSelectStyles}
placeholder={this.props.intl.formatMessage(messages.select)}
theme={selectTheme}
components={{ DropdownIndicator, Option }}
onChange={(data) => {
this.onChangeValue(index, data.value);
}}
isMulti={false}
value={
relativePathOptions.filter((p) => p.value === props.value)?.[0]
}
/>
</Form.Field>
);
default:
// if (row.o === 'plone.app.querystring.operation.string.relativePath') {
// props.onChange = data => this.onChangeValue(index, data.target.value);
// }
return (
<Form.Field style={{ flex: '1 0 auto' }}>
<Input {...props} description={operator.description} />
</Form.Field>
);
}
}
/**
* Change value handler
* @method onChangeValue
* @param {Number} index Index of the row.
* @param {String|Array} value Value of the row.
* @returns {undefined}
*/
onChangeValue(index, value) {
this.props.onChange(
this.props.id,
map(this.props.value, (row, i) =>
index === i
? {
...row,
v: value,
}
: row,
),
);
}
/**
* Render method.
* @method render
* @returns {string} Markup for the component.
*/
render() {
const {
id,
required,
description,
error,
value,
onChange,
onEdit,
indexes,
fieldSet,
reactSelect,
intl,
} = this.props;
const Select = reactSelect.default;
return (
<Form.Field
inline
required={required}
error={error.length > 0}
className={cx('query-widget', description ? 'help' : '')}
id={`${fieldSet || 'field'}-${id}`}
>
<Grid>
<Grid.Row stretched>
<Grid.Column width="12">
<div className="simple-field-name">
{intl.formatMessage(messages.Criteria)}
</div>
</Grid.Column>
</Grid.Row>
<Grid.Row stretched>
<Grid.Column width="12">
{indexes &&
!isEmpty(indexes) &&
map(value, (row, index) => (
<Form.Group key={index}>
<div className="main-fields-wrapper">
<Form.Field
style={{ flex: '1 0 auto', marginRight: '10px' }}
>
<Select
id={`field-${id}`}
name={id}
disabled={onEdit !== null}
className="react-select-container"
classNamePrefix="react-select"
options={map(
toPairs(
groupBy(
toPairs(indexes),
(item) => item[1].group,
),
),
(group) => ({
label: group[0],
options: map(
filter(group[1], (item) => item[1].enabled),
(field) => ({
label: field[1].title,
value: field[0],
isDisabled: (value || []).some(
(v) =>
v['i'] !== 'path' && v['i'] === field[0],
),
}),
),
}),
)}
styles={customSelectStyles}
theme={selectTheme}
placeholder={intl.formatMessage(messages.select)}
components={{ DropdownIndicator, Option }}
value={{
value: row.i,
label: indexes[row.i]?.title,
}}
onChange={(data) => {
onChange(
id,
map(value, (curRow, curIndex) =>
curIndex === index
? {
i: data.value,
o: indexes[data.value].operations[0],
v: '',
}
: curRow,
),
);
}}
/>
</Form.Field>
<Form.Field style={{ flex: '1 0 auto' }}>
<Select
id={`field-${id}`}
name={id}
disabled={onEdit !== null}
className="react-select-container"
classNamePrefix="react-select"
options={map(
indexes[row.i]?.operations ?? [],
(operation) => ({
value: operation,
label: indexes[row.i].operators[operation].title,
}),
)}
styles={customSelectStyles}
theme={selectTheme}
placeholder={intl.formatMessage(messages.select)}
components={{ DropdownIndicator, Option }}
value={{
value: row.o,
label: indexes[row.i].operators[row.o].title,
}}
onChange={(data) =>
onChange(
id,
map(value, (curRow, curIndex) =>
curIndex === index
? {
i: row.i,
o: data.value,
v: '',
}
: curRow,
),
)
}
/>
</Form.Field>
{!this.props.indexes[row.i].operators[row.o].widget && (
<Button
onClick={(event) => {
onChange(
id,
remove(value, (v, i) => i !== index),
);
event.preventDefault();
}}
style={{
background: 'none',
paddingRight: 0,
paddingLeft: 0,
margin: 0,
}}
>
<Icon name={clearSVG} size="24px" className="close" />
</Button>
)}
</div>
{this.getWidget(row, index, Select, intl)}
{this.props.indexes[row.i].operators[row.o].widget && (
<Button
onClick={(event) => {
onChange(
id,
remove(value, (v, i) => i !== index),
);
event.preventDefault();
}}
style={{
background: 'none',
paddingRight: 0,
paddingLeft: 0,
margin: 0,
}}
>
<Icon name={clearSVG} size="24px" className="close" />
</Button>
)}
</Form.Group>
))}
<Form.Group>
<Form.Field style={{ flex: '1 0 auto' }}>
<Select
id={`field-${id}`}
name={id}
disabled={onEdit !== null}
className="react-select-container"
classNamePrefix="react-select"
placeholder={intl.formatMessage(messages.AddCriteria)}
options={map(
toPairs(
groupBy(toPairs(indexes), (item) => item[1].group),
),
(group) => ({
label: group[0],
options: map(
filter(group[1], (item) => item[1].enabled),
(field) => ({
label: field[1].title,
value: field[0],
// disable selecting indexes that are already used,
// except for path, which has explicit support
// in the backend for multipath queries
isDisabled: (value || []).some(
(v) => v['i'] !== 'path' && v['i'] === field[0],
),
}),
),
}),
)}
styles={customSelectStyles}
theme={selectTheme}
components={{ DropdownIndicator, Option }}
value={null}
onChange={(data) => {
onChange(id, [
...(value || []),
{
i: data.value,
o: indexes[data.value].operations[0],
v: '',
},
]);
}}
/>
</Form.Field>
</Form.Group>
{map(error, (message) => (
<Label key={message} basic color="red" pointing>
{message}
</Label>
))}
</Grid.Column>
</Grid.Row>
{description && (
<Grid.Row stretched>
<Grid.Column stretched width="12">
<p className="help">{description}</p>
</Grid.Column>
</Grid.Row>
)}
</Grid>
</Form.Field>
);
}
}
export default compose(
injectIntl,
injectLazyLibs(['reactSelect']),
withRouter,
connect(
(state, props) => ({
indexes: state.querystring.indexes,
reference: state.querystringsearch.subrequests,
}),
{ getQuerystring, getQueryStringResults },
),
)(QuerystringWidgetComponent);