Skip to content

Commit

Permalink
Merge pull request #764 from neos/full-select-editor
Browse files Browse the repository at this point in the history
FEATURE: Full select editor (with data providers and multiselect support)
  • Loading branch information
skurfuerst authored Sep 6, 2017
2 parents 5e5559e + 4100cfd commit 16b22c8
Show file tree
Hide file tree
Showing 17 changed files with 638 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ protected function convertBoolean($rawValue)
*/
protected function convertArray($rawValue)
{
return json_decode($rawValue, true);
if (is_string($rawValue)) {
return json_decode($rawValue, true);
}
return $rawValue;
}
}
8 changes: 7 additions & 1 deletion packages/neos-ui-backend-connector/src/Endpoints/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@ const setUserPreferences = csrfToken => (key, value) => {
});
};

const dataSource = (dataSourceIdentifier, dataSourceUri, params = {}) => fetchJson(urlWithParams(dataSourceUri || '/neos/service/data-source/' + dataSourceIdentifier, params), {
method: 'GET',
credentials: 'include'
});

export default csrfToken => ({
loadImageMetadata,
change: change(csrfToken),
Expand All @@ -205,5 +210,6 @@ export default csrfToken => ({
searchNodes,
getSingleNode,
adoptNodeToOtherDimension: adoptNodeToOtherDimension(csrfToken),
setUserPreferences: setUserPreferences(csrfToken)
setUserPreferences: setUserPreferences(csrfToken),
dataSource
});
6 changes: 5 additions & 1 deletion packages/neos-ui-editors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@
"@neos-project/neos-ui-i18n": "1.0.0-beta3",
"@neos-project/neos-ui-inspector": "1.0.0-beta3",
"@neos-project/neos-ui-redux-store": "1.0.0-beta3",
"@neos-project/react-ui-components": "1.0.0-beta3"
"@neos-project/react-ui-components": "1.0.0-beta3",
"@neos-project/neos-ui-decorators": "1.0.0-beta3"
},
"license": "GNU GPLv3",
"jest": {
"transformIgnorePatterns": [],
"transform": {
"neos-ui-editors/src/.+\\.jsx?$": "./node_modules/.bin/babel-jest",
"node_modules/@neos-project/.+\\.jsx?$": "./node_modules/.bin/babel-jest"
},
"moduleNameMapper": {
"\\.css$": "identity-obj-proxy"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {$transform} from 'plow-js';
import {connect} from 'react-redux';
import SelectBox from '@neos-project/react-ui-components/src/SelectBox/';
import MultiSelectBox from '@neos-project/react-ui-components/src/MultiSelectBox/';
import {selectors} from '@neos-project/neos-ui-redux-store';
import {neos} from '@neos-project/neos-ui-decorators';
import {shouldDisplaySearchBox, searchOptions, processSelectBoxOptions} from './SelectBoxHelpers';

@neos(globalRegistry => ({
i18nRegistry: globalRegistry.get('i18n'),
dataSourcesDataLoader: globalRegistry.get('dataLoaders').get('DataSources')
}))
@connect($transform({
focusedNodePath: selectors.CR.Nodes.focusedNodePathSelector
}))
export default class DataSourceBasedSelectBoxEditor extends PureComponent {
static propTypes = {
commit: PropTypes.func.isRequired,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string)
]),
options: PropTypes.shape({
allowEmpty: PropTypes.bool,
placeholder: PropTypes.string,

multiple: PropTypes.bool,

dataSourceIdentifier: PropTypes.string,
dataSourceUri: PropTypes.string,
dataSourceAdditionalData: PropTypes.objectOf(PropTypes.any),

minimumResultsForSearch: PropTypes.number,

values: PropTypes.objectOf(
PropTypes.shape({
label: PropTypes.string,
icon: PropTypes.string,

// TODO
group: PropTypes.string
})
)

}).isRequired,

i18nRegistry: PropTypes.object.isRequired,
dataSourcesDataLoader: PropTypes.shape({
resolveValue: PropTypes.func.isRequired
}).isRequired,

focusedNodePath: PropTypes.string.isRequired
};

static defaultOptions = {
// Use "5" as minimum result for search default; same as with old UI
minimumResultsForSearch: 5
};

constructor(props) {
super(props);

this.state = {
searchTerm: '',
isLoading: false,
selectBoxOptions: {}
};
}

getDataLoaderOptions() {
return {
contextNodePath: this.props.focusedNodePath,
dataSourceIdentifier: this.props.options.dataSourceIdentifier,
dataSourceUri: this.props.options.dataSourceUri,
dataSourceAdditionalData: this.props.options.dataSourceAdditionalData
};
}

componentDidMount() {
this.setState({isLoading: true});
this.props.dataSourcesDataLoader.resolveValue(this.getDataLoaderOptions(), this.props.value)
.then(selectBoxOptions => {
this.setState({
isLoading: false,
selectBoxOptions
});
});
}

render() {
const {commit, value, i18nRegistry} = this.props;
const options = Object.assign({}, this.defaultOptions, this.props.options);

const processedSelectBoxOptions = processSelectBoxOptions(i18nRegistry, this.state.selectBoxOptions);

// Placeholder text must be unescaped in case html entities were used
const placeholder = options && options.placeholder && i18nRegistry.translate(unescape(options.placeholder));

if (options.multiple) {
return (<MultiSelectBox
options={processedSelectBoxOptions}
values={value || []}
onValuesChange={commit}
displayLoadingIndicator={this.state.isLoading}
placeholder={placeholder}

allowEmpty={options.allowEmpty}
displaySearchBox={shouldDisplaySearchBox(options, processedSelectBoxOptions)}
searchOptions={searchOptions(this.state.searchTerm, processedSelectBoxOptions)}
searchTerm={this.state.searchTerm}
onSearchTermChange={this.handleSearchTermChange}
/>);
}

// multiple = FALSE
return (<SelectBox
options={this.state.searchTerm ? searchOptions(this.state.searchTerm, processedSelectBoxOptions) : processedSelectBoxOptions}
value={value}
onValueChange={commit}
displayLoadingIndicator={this.state.isLoading}
placeholder={placeholder}

allowEmpty={options.allowEmpty}
displaySearchBox={shouldDisplaySearchBox(options, processedSelectBoxOptions)}
searchTerm={this.state.searchTerm}
onSearchTermChange={this.handleSearchTermChange}
/>);
}

handleSearchTermChange = searchTerm => {
this.setState({searchTerm});
}
}
16 changes: 16 additions & 0 deletions packages/neos-ui-editors/src/SelectBox/SelectBoxHelpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const shouldDisplaySearchBox = (options, processedSelectBoxOptions) => options.minimumResultsForSearch >= 0 && processedSelectBoxOptions.length >= options.minimumResultsForSearch;

// Currently, we're doing an extremely simple lowercase substring matching; of course this could be improved a lot!
export const searchOptions = (searchTerm, processedSelectBoxOptions) =>
processedSelectBoxOptions.filter(option => option.label && option.label.toLowerCase().indexOf(searchTerm.toLowerCase()) !== -1);

export const processSelectBoxOptions = (i18nRegistry, selectBoxOptions) =>
Object.keys(selectBoxOptions)
.filter(k => selectBoxOptions[k])
// Filter out items without a label
.map(k => selectBoxOptions[k].label && Object.assign(
{value: k},
selectBoxOptions[k],
{label: i18nRegistry.translate(selectBoxOptions[k].label)}
))
.filter(k => k);
91 changes: 91 additions & 0 deletions packages/neos-ui-editors/src/SelectBox/SimpleSelectBoxEditor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import SelectBox from '@neos-project/react-ui-components/src/SelectBox/';
import MultiSelectBox from '@neos-project/react-ui-components/src/MultiSelectBox/';
import {neos} from '@neos-project/neos-ui-decorators';
import {shouldDisplaySearchBox, searchOptions, processSelectBoxOptions} from './SelectBoxHelpers';

@neos(globalRegistry => ({
i18nRegistry: globalRegistry.get('i18n')
}))
export default class SimpleSelectBoxEditor extends PureComponent {
static propTypes = {
commit: PropTypes.func.isRequired,
value: PropTypes.any,
options: PropTypes.shape({
allowEmpty: PropTypes.bool,
placeholder: PropTypes.string,

multiple: PropTypes.bool,

minimumResultsForSearch: PropTypes.number,

values: PropTypes.objectOf(
PropTypes.shape({
label: PropTypes.string,
icon: PropTypes.string,

// TODO
group: PropTypes.string
})
)

}).isRequired,

i18nRegistry: PropTypes.object.isRequired
};

static defaultOptions = {
// Use "5" as minimum result for search default; same as with old UI
minimumResultsForSearch: 5
};

constructor(props) {
super(props);

this.state = {
searchTerm: ''
};
}

render() {
const {commit, value, i18nRegistry} = this.props;
const options = Object.assign({}, this.defaultOptions, this.props.options);

const processedSelectBoxOptions = processSelectBoxOptions(i18nRegistry, options.values);

// Placeholder text must be unescaped in case html entities were used
const placeholder = options && options.placeholder && i18nRegistry.translate(unescape(options.placeholder));

if (options.multiple) {
return (<MultiSelectBox
options={processedSelectBoxOptions}
values={value || []}
onValuesChange={commit}
placeholder={placeholder}
allowEmpty={options.allowEmpty}
displaySearchBox={shouldDisplaySearchBox(options, processedSelectBoxOptions)}
searchOptions={searchOptions(this.state.searchTerm, processedSelectBoxOptions)}
searchTerm={this.state.searchTerm}
onSearchTermChange={this.handleSearchTermChange}
/>);
}

// multiple == FALSE
return (<SelectBox
options={this.state.searchTerm ? searchOptions(this.state.searchTerm, processedSelectBoxOptions) : processedSelectBoxOptions}
value={value}
onValueChange={commit}
placeholder={placeholder}

allowEmpty={options.allowEmpty}
displaySearchBox={shouldDisplaySearchBox(options, processedSelectBoxOptions)}
searchTerm={this.state.searchTerm}
onSearchTermChange={this.handleSearchTermChange}
/>);
}

handleSearchTermChange = searchTerm => {
this.setState({searchTerm});
}
}
40 changes: 11 additions & 29 deletions packages/neos-ui-editors/src/SelectBox/index.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,22 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import I18n from '@neos-project/neos-ui-i18n';
import SelectBox from '@neos-project/react-ui-components/src/SelectBox/';
import {neos} from '@neos-project/neos-ui-decorators';
import SimpleSelectBoxEditor from './SimpleSelectBoxEditor';
import DataSourceBasedSelectBoxEditor from './DataSourceBasedSelectBoxEditor';

@neos(globalRegistry => ({
i18nRegistry: globalRegistry.get('i18n')
}))
export default class SelectBoxEditor extends PureComponent {
static propTypes = {
commit: PropTypes.func.isRequired,
value: PropTypes.any,
options: PropTypes.any.isRequired,

i18nRegistry: PropTypes.object.isRequired
options: PropTypes.shape({
dataSourceIdentifier: PropTypes.string,
dataSourceUri: PropTypes.string
}).isRequired
};

render() {
const {commit, value, options, i18nRegistry} = this.props;
const selectBoxOptions = Object.keys(options.values)
.filter(k => options.values[k])
// Filter out items without a label
.map(k => options.values[k].label && Object.assign(
{value: k},
options.values[k],
{label: <I18n id={options.values[k].label}/>}
)
).filter(k => k);
// Placeholder text must be unescaped in case html entities were used
const placeholder = options && options.placeholder && i18nRegistry.translate(unescape(options.placeholder));
const {options} = this.props;

return (<SelectBox
options={selectBoxOptions}
value={value}
onValueChange={commit}
placeholder={placeholder}
/>);
if (options.dataSourceIdentifier || options.dataSourceUri) {
return <DataSourceBasedSelectBoxEditor {...this.props}/>;
}
return <SimpleSelectBoxEditor {...this.props}/>;
}
}
Loading

0 comments on commit 16b22c8

Please sign in to comment.