UNPKG

@plone/volto

Version:
638 lines (619 loc) 21.6 kB
/** * 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);