diff --git a/openbis_ng_ui/src/js/components/common/form/AutocompleterField.jsx b/openbis_ng_ui/src/js/components/common/form/AutocompleterField.jsx index 78471eccb96d869beae60a6cc37ec018d2044143..f526082d1eb42ed390a27332f79f20ec3edd0bfc 100644 --- a/openbis_ng_ui/src/js/components/common/form/AutocompleterField.jsx +++ b/openbis_ng_ui/src/js/components/common/form/AutocompleterField.jsx @@ -5,6 +5,7 @@ import TextField from '@material-ui/core/TextField' import InputAdornment from '@material-ui/core/InputAdornment' import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp' import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown' +import CircularProgress from '@material-ui/core/CircularProgress' import FormFieldContainer from '@src/js/components/common/form/FormFieldContainer.jsx' import FormFieldLabel from '@src/js/components/common/form/FormFieldLabel.jsx' import FormFieldView from '@src/js/components/common/form/FormFieldView.jsx' @@ -30,6 +31,11 @@ const styles = theme => ({ } }, adornment: { + marginRight: '-32px', + marginTop: '-16px', + color: '#0000008a' + }, + adornmentFreeSolo: { marginRight: '-4px', marginTop: '-16px', color: '#0000008a' @@ -38,6 +44,7 @@ const styles = theme => ({ class AutocompleterFormField extends React.PureComponent { static defaultProps = { + freeSolo: false, mode: 'edit', variant: 'filled' } @@ -54,6 +61,7 @@ class AutocompleterFormField extends React.PureComponent { this.handleClick = this.handleClick.bind(this) this.handleKeyDown = this.handleKeyDown.bind(this) this.handleChange = this.handleChange.bind(this) + this.handleInputChange = this.handleInputChange.bind(this) this.handleFocus = this.handleFocus.bind(this) this.handleBlur = this.handleBlur.bind(this) } @@ -99,6 +107,14 @@ class AutocompleterFormField extends React.PureComponent { this.handleEvent(event, value, this.props.onChange) } + handleInputChange(event, value) { + this.setState({ + open: true + }) + + this.handleEvent(event, value, this.props.onInputChange) + } + handleFocus(event) { this.setState({ focused: true @@ -112,10 +128,6 @@ class AutocompleterFormField extends React.PureComponent { open: false }) - if (event.target.value !== this.props.value) { - this.handleEvent(event, event.target.value, this.props.onChange) - } - event.persist() setTimeout(() => { @@ -162,12 +174,19 @@ class AutocompleterFormField extends React.PureComponent { name, description, value, + inputValue, + freeSolo, disabled, error, metadata, styles, classes, - variant + variant, + renderOption, + filterOptions, + getOptionLabel, + getOptionSelected, + getOptionDisabled } = this.props const { open, focused } = this.state @@ -181,14 +200,21 @@ class AutocompleterFormField extends React.PureComponent { onClick={this.handleClick} > <Autocomplete - freeSolo disableClearable + freeSolo={freeSolo} name={name} disabled={disabled} options={this.getOptions()} + renderOption={renderOption} + filterOptions={filterOptions} + getOptionLabel={getOptionLabel} + getOptionSelected={getOptionSelected} + getOptionDisabled={getOptionDisabled} value={value} + inputValue={inputValue} open={open} onChange={this.handleChange} + onInputChange={this.handleInputChange} onFocus={this.handleFocus} onBlur={this.handleBlur} onKeyDown={this.handleKeyDown} @@ -242,23 +268,32 @@ class AutocompleterFormField extends React.PureComponent { renderAdornment() { const { open } = this.state - const { classes } = this.props + const { loading, freeSolo, classes } = this.props + return ( - <InputAdornment position='end' classes={{ root: classes.adornment }}> + <InputAdornment + position='end' + classes={{ + root: freeSolo ? classes.adornmentFreeSolo : classes.adornment + }} + > + {loading ? <CircularProgress color='inherit' size={20} /> : null} {open ? <ArrowDropUpIcon /> : <ArrowDropDownIcon />} </InputAdornment> ) } getOptions() { - const { options, sort = true } = this.props + const { options, getOptionLabel, sort = true } = this.props if (options) { let result = Array.from(options) if (sort) { result.sort((option1, option2) => { - return compare(option1, option2) + let label1 = getOptionLabel ? getOptionLabel(option1) : option1 + let label2 = getOptionLabel ? getOptionLabel(option2) : option2 + return compare(label1, label2) }) } diff --git a/openbis_ng_ui/src/js/components/common/form/EntityAutocompleterField.jsx b/openbis_ng_ui/src/js/components/common/form/EntityAutocompleterField.jsx new file mode 100644 index 0000000000000000000000000000000000000000..31abf0a451a331a893a5c3949adf03306a3446d6 --- /dev/null +++ b/openbis_ng_ui/src/js/components/common/form/EntityAutocompleterField.jsx @@ -0,0 +1,328 @@ +import _ from 'lodash' +import React from 'react' +import autoBind from 'auto-bind' +import { withStyles } from '@material-ui/core/styles' +import AutocompleterField from '@src/js/components/common/form/AutocompleterField.jsx' +import openbis from '@src/js/services/openbis.js' +import logger from '@src/js/common/logger.js' + +const styles = () => ({}) + +const LOADED_OPTIONS_COUNT = 100 + +class EntityAutocompleterField extends React.PureComponent { + constructor(props) { + super(props) + + autoBind(this) + + this.state = { + loading: false, + inputValue: this.getOptionLabel(props.value), + options: [] + } + } + + async load(value) { + this.cancelScheduledLoad() + + const loadId = setTimeout(async () => { + try { + logger.log( + logger.DEBUG, + `EntityAutocompleterField - executing load request (id: ${loadId}, value: ${value})` + ) + + const results = await this.loadEntities(value, LOADED_OPTIONS_COUNT) + + let options = [] + if (results.options.length > 0) { + if (results.totalCount > results.options.length) { + options.push({ + label: `Showing only the first ${LOADED_OPTIONS_COUNT} results (${results.totalCount} found)` + }) + } + results.options.forEach(option => options.push(option)) + } else { + options = [ + { + label: 'No entities found' + } + ] + } + + if (loadId === this.state.loadId) { + logger.log( + logger.DEBUG, + `EntityAutocompleterField - received valid load response (id: ${loadId}, value: ${value})` + ) + + this.setState({ + loading: false, + options + }) + } else { + logger.log( + logger.DEBUG, + `EntityAutocompleterField - ignoring old load response (id: ${loadId}, value: ${value})` + ) + } + } catch (error) { + this.setState({ + loading: false + }) + } + }, 250) + + logger.log( + logger.DEBUG, + `EntityAutocompleterField - scheduled load request (id: ${loadId}, value: ${value})` + ) + + this.setState({ + loading: true, + loadId + }) + } + + cancelScheduledLoad() { + const lastLoadId = this.state.loadId + + if (lastLoadId) { + logger.log( + logger.DEBUG, + `EntityAutocompleterField - cancelling scheduled load request (id: ${lastLoadId})` + ) + + clearTimeout(lastLoadId) + this.setState({ + loadId: null + }) + } + } + + async loadEntities(value, count) { + const { entityKind } = this.props + + if (entityKind === openbis.EntityKind.EXPERIMENT) { + return await this.loadExperiments(value, count) + } else if (entityKind === openbis.EntityKind.SAMPLE) { + return await this.loadSamples(value, count) + } else if (entityKind === openbis.EntityKind.DATA_SET) { + return await this.loadDataSets(value, count) + } else if (entityKind === openbis.EntityKind.MATERIAL) { + return await this.loadMaterials(value, count) + } else { + return [] + } + } + + async loadExperiments(value, count) { + const criteria = new openbis.ExperimentSearchCriteria() + criteria.withOrOperator() + + if (value && value.trim().length > 0) { + //criteria.withCode().thatContains(value) + criteria.withIdentifier().thatContains(value) + criteria.withProperty('$NAME').thatContains(value) + } + + const fo = new openbis.ExperimentFetchOptions() + fo.from(0).count(count) + fo.sortBy().identifier().asc() + + const results = await openbis.searchExperiments(criteria, fo) + + return { + options: results.getObjects().map(object => ({ + label: object.identifier.identifier, + entityKind: openbis.EntityKind.EXPERIMENT, + entityId: object.identifier.identifier + })), + totalCount: results.totalCount + } + } + + async loadSamples(value, count) { + const criteria = new openbis.SampleSearchCriteria() + criteria.withOrOperator() + + if (value && value.trim().length > 0) { + //criteria.withCode().thatContains(value) + criteria.withIdentifier().thatContains(value) + criteria.withProperty('$NAME').thatContains(value) + } + + const fo = new openbis.SampleFetchOptions() + fo.from(0).count(count) + fo.sortBy().identifier().asc() + + const results = await openbis.searchSamples(criteria, fo) + + return { + options: results.getObjects().map(object => ({ + label: object.identifier.identifier, + entityKind: openbis.EntityKind.SAMPLE, + entityId: object.identifier.identifier + })), + totalCount: results.totalCount + } + } + + async loadMaterials(value, count) { + const criteria = new openbis.MaterialSearchCriteria() + criteria.withOrOperator() + + if (value && value.trim().length > 0) { + criteria.withCode().thatContains(value) + criteria.withProperty('$NAME').thatContains(value) + } + + const fo = new openbis.MaterialFetchOptions() + fo.from(0).count(count) + fo.sortBy().code().asc() + + const results = await openbis.searchMaterials(criteria, fo) + + return { + options: results.getObjects().map(object => ({ + label: object.permId.code + ' (' + object.permId.typeCode + ')', + entityKind: openbis.EntityKind.MATERIAL, + entityId: { + code: object.permId.code, + typeCode: object.permId.typeCode + } + })), + totalCount: results.totalCount + } + } + + async loadDataSets(value, count) { + const criteria = new openbis.DataSetSearchCriteria() + criteria.withOrOperator() + + if (value && value.trim().length > 0) { + criteria.withCode().thatContains(value) + criteria.withProperty('$NAME').thatContains(value) + + const experimentCriteria = criteria.withExperiment() + experimentCriteria.withOrOperator() + //experimentCriteria.withCode().thatContains(value) + experimentCriteria.withIdentifier().thatContains(value) + experimentCriteria.withProperty('$NAME').thatContains(value) + + const sampleCriteria = criteria.withSample() + sampleCriteria.withOrOperator() + //sampleCriteria.withCode().thatContains(value) + sampleCriteria.withIdentifier().thatContains(value) + sampleCriteria.withProperty('$NAME').thatContains(value) + } + + const fo = new openbis.DataSetFetchOptions() + fo.from(0).count(count) + fo.sortBy().code().asc() + + const results = await openbis.searchDataSets(criteria, fo) + + return { + options: results.getObjects().map(object => ({ + label: object.code, + entityKind: openbis.EntityKind.DATA_SET, + entityId: object.code + })), + totalCount: results.totalCount + } + } + + handleFocus() { + this.load(this.state.inputValue) + } + + handleInputChange(event) { + this.setState({ + inputValue: event.target.value + }) + this.load(event.target.value) + } + + handleBlur(event) { + const { value, onChange } = this.props + const { inputValue } = this.state + + const valueTrimmed = value ? this.getOptionLabel(value).trim() : '' + const inputValueTrimmed = inputValue ? inputValue.trim() : '' + + if (inputValueTrimmed.length === 0 && valueTrimmed.length !== 0) { + if (onChange) { + onChange(event) + } + } else if (inputValueTrimmed !== valueTrimmed) { + this.setState({ + inputValue: this.getOptionLabel(value) + }) + } + + this.cancelScheduledLoad() + + this.setState({ + loading: false, + options: [] + }) + } + + renderOption(option) { + return <span>{this.getOptionLabel(option)}</span> + } + + filterOptions(options) { + // do not filter options on the client side + return options + } + + getOptionLabel(option) { + if (option) { + return option.label + } else { + return '' + } + } + + getOptionSelected(option, value) { + return ( + _.isEqual(option.entityKind, value.entityKind) && + _.isEqual(option.entityId, value.entityId) + ) + } + + getOptionDisabled(option) { + return !option.entityId + } + + render() { + logger.log(logger.DEBUG, 'EntityAutocompleterField.render') + + const { value } = this.props + const { loading, inputValue, options } = this.state + + return ( + <AutocompleterField + {...this.props} + loading={loading} + freeSolo={true} + value={value} + inputValue={inputValue} + renderOption={this.renderOption} + filterOptions={this.filterOptions} + getOptionLabel={this.getOptionLabel} + getOptionSelected={this.getOptionSelected} + getOptionDisabled={this.getOptionDisabled} + onInputChange={this.handleInputChange} + onFocus={this.handleFocus} + onBlur={this.handleBlur} + options={options} + /> + ) + } +} + +export default withStyles(styles)(EntityAutocompleterField) diff --git a/openbis_ng_ui/src/js/components/tools/form/plugin/PluginFormControllerEvaluate.js b/openbis_ng_ui/src/js/components/tools/form/plugin/PluginFormControllerEvaluate.js index 0198713edd3e8b42289513c98bee9184e38a3cd8..8c8514574901115aa6fc35546dc4116e54ac9a57 100644 --- a/openbis_ng_ui/src/js/components/tools/form/plugin/PluginFormControllerEvaluate.js +++ b/openbis_ng_ui/src/js/components/tools/form/plugin/PluginFormControllerEvaluate.js @@ -50,8 +50,7 @@ export default class PluginFormControllerEvaluate { const pluginName = plugin.name.value const pluginScript = plugin.script.value - const entityKind = evaluateParameters.entityKind.value - const entityId = evaluateParameters.entityId.value + const entity = evaluateParameters.entity.value const entityIsNew = evaluateParameters.entityIsNew.value let options = null @@ -63,20 +62,24 @@ export default class PluginFormControllerEvaluate { options.setNew(entityIsNew) } - let objectId = null + if (entity) { + let objectId = null - if (entityKind === openbis.EntityKind.EXPERIMENT) { - objectId = new openbis.ExperimentIdentifier(entityId) - } else if (entityKind === openbis.EntityKind.SAMPLE) { - objectId = new openbis.SampleIdentifier(entityId) - } else if (entityKind === openbis.EntityKind.DATA_SET) { - objectId = new openbis.DataSetPermId(entityId) - } else if (entityKind === openbis.EntityKind.MATERIAL) { - const entityIdParts = entityId.split(' ') - objectId = new openbis.MaterialPermId(entityIdParts[0], entityIdParts[1]) - } + if (entity.entityKind === openbis.EntityKind.EXPERIMENT) { + objectId = new openbis.ExperimentIdentifier(entity.entityId) + } else if (entity.entityKind === openbis.EntityKind.SAMPLE) { + objectId = new openbis.SampleIdentifier(entity.entityId) + } else if (entity.entityKind === openbis.EntityKind.DATA_SET) { + objectId = new openbis.DataSetPermId(entity.entityId) + } else if (entity.entityKind === openbis.EntityKind.MATERIAL) { + objectId = new openbis.MaterialPermId( + entity.entityId.code, + entity.entityId.typeCode + ) + } - options.setObjectId(objectId) + options.setObjectId(objectId) + } if (mode === PageMode.VIEW) { options.setPluginId(new openbis.PluginPermId(pluginName)) diff --git a/openbis_ng_ui/src/js/components/tools/form/plugin/PluginFormControllerLoad.js b/openbis_ng_ui/src/js/components/tools/form/plugin/PluginFormControllerLoad.js index 20cedda8e63556c96d3ecd61ab4b7e59c786e8e1..fee629cb6e1479472de0e94c72a162a38ed4596c 100644 --- a/openbis_ng_ui/src/js/components/tools/form/plugin/PluginFormControllerLoad.js +++ b/openbis_ng_ui/src/js/components/tools/form/plugin/PluginFormControllerLoad.js @@ -77,7 +77,7 @@ export default class PluginFormControllerLoad extends PageControllerLoad { _createEvaluateParameters() { return { entityKind: FormUtil.createField(), - entityId: FormUtil.createField(), + entity: FormUtil.createField(), entityIsNew: FormUtil.createField() } } diff --git a/openbis_ng_ui/src/js/components/tools/form/plugin/PluginFormEvaluateParameters.jsx b/openbis_ng_ui/src/js/components/tools/form/plugin/PluginFormEvaluateParameters.jsx index 819ff2131351de77c07e162cfc3f0dbfea15c885..ca5e7c251b0a12e7afcdd99647ae5fbfd3d4fe20 100644 --- a/openbis_ng_ui/src/js/components/tools/form/plugin/PluginFormEvaluateParameters.jsx +++ b/openbis_ng_ui/src/js/components/tools/form/plugin/PluginFormEvaluateParameters.jsx @@ -2,7 +2,7 @@ import React from 'react' import { withStyles } from '@material-ui/core/styles' import Container from '@src/js/components/common/form/Container.jsx' import Header from '@src/js/components/common/form/Header.jsx' -import AutocompleterField from '@src/js/components/common/form/AutocompleterField.jsx' +import EntityAutocompleterField from '@src/js/components/common/form/EntityAutocompleterField.jsx' import SelectField from '@src/js/components/common/form/SelectField.jsx' import CheckboxField from '@src/js/components/common/form/CheckboxField.jsx' import PluginFormSelectionType from '@src/js/components/tools/form/plugin/PluginFormSelectionType.js' @@ -35,7 +35,7 @@ class PluginFormEvaluateParameters extends React.PureComponent { <Container> <Header>Tester</Header> {this.renderEntityKind()} - {this.renderEntityId()} + {this.renderEntity()} {this.renderEntityIsNew()} </Container> ) @@ -64,18 +64,17 @@ class PluginFormEvaluateParameters extends React.PureComponent { ) } - renderEntityId() { + renderEntity() { const { parameters, classes } = this.props - const options = [] - return ( <div className={classes.parameter}> - <AutocompleterField + <EntityAutocompleterField + key={parameters.entityKind.value} label='Entity' - name='entityId' - options={options} - value={parameters.entityId.value} + name='entity' + entityKind={parameters.entityKind.value} + value={parameters.entity.value} onChange={this.handleChange} /> </div> diff --git a/openbis_ng_ui/src/js/components/types/form/TypeFormParametersProperty.jsx b/openbis_ng_ui/src/js/components/types/form/TypeFormParametersProperty.jsx index cef4329c7094cafe7ab1d340d242e71308bb9d36..830a3b2b4b093e7e21e8a140caa4d86d4973314f 100644 --- a/openbis_ng_ui/src/js/components/types/form/TypeFormParametersProperty.jsx +++ b/openbis_ng_ui/src/js/components/types/form/TypeFormParametersProperty.jsx @@ -299,7 +299,7 @@ class TypeFormParametersProperty extends React.PureComponent { disabled={!enabled} value={value} mode={mode} - onChange={this.handleChange} + onInputChange={this.handleChange} onFocus={this.handleFocus} onBlur={this.handleBlur} /> diff --git a/openbis_ng_ui/src/js/services/openbis/api.js b/openbis_ng_ui/src/js/services/openbis/api.js index 8780def033fe4a0cf6e0e31fe27247a284a455c8..6342a38da5b52c4178b56d5f9bb5b096ff4962f7 100644 --- a/openbis_ng_ui/src/js/services/openbis/api.js +++ b/openbis_ng_ui/src/js/services/openbis/api.js @@ -87,6 +87,14 @@ class Facade { return this.promise(this.v3.searchSamples(criteria, fo)) } + searchExperiments(criteria, fo) { + return this.promise(this.v3.searchExperiments(criteria, fo)) + } + + searchDataSets(criteria, fo) { + return this.promise(this.v3.searchDataSets(criteria, fo)) + } + searchVocabularies(criteria, fo) { return this.promise(this.v3.searchVocabularies(criteria, fo)) }