├── .editorconfig ├── .gitignore ├── .nvmrc ├── Classes └── LazyDataSourceTrait.php ├── Configuration └── Settings.yaml ├── README.md ├── Resources ├── Private │ └── JavaScript │ │ └── LazyDataSource │ │ ├── package.json │ │ ├── src │ │ ├── Editors │ │ │ ├── DataSourceSelectEditor.js │ │ │ ├── PreviewOption.js │ │ │ └── lazyDataSourceDataLoader.js │ │ ├── index.js │ │ └── manifest.js │ │ └── yarn.lock └── Public │ └── JavaScript │ └── LazyDataSource │ └── Plugin.js ├── composer.json └── dev.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.js] 2 | indent_style = space 3 | indent_size = 4 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /Classes/LazyDataSourceTrait.php: -------------------------------------------------------------------------------- 1 | getDataForIdentifiers($identifiers, $node, $arguments); 20 | } elseif (isset($arguments['searchTerm'])) { 21 | $searchTerm = $arguments['searchTerm']; 22 | unset($arguments['searchTerm']); 23 | return $this->searchData($searchTerm, $node, $arguments); 24 | } 25 | 26 | return []; 27 | } 28 | 29 | /** 30 | * This method is called when the identifiers are known (from the client-side); and we need to load 31 | * these data records specifically. 32 | * 33 | * @param array $identifiers 34 | * @param Node|null $node 35 | * @param array $arguments 36 | * @return mixed 37 | */ 38 | abstract protected function getDataForIdentifiers(array $identifiers, Node $node = null, array $arguments = []); 39 | 40 | /** 41 | * This method is called when the user specifies a search term. 42 | * 43 | * @param string $searchTerm 44 | * @param Node|null $node 45 | * @param array $arguments 46 | * @return mixed 47 | */ 48 | abstract protected function searchData(string $searchTerm, Node $node = null, array $arguments = []); 49 | } 50 | -------------------------------------------------------------------------------- /Configuration/Settings.yaml: -------------------------------------------------------------------------------- 1 | Neos: 2 | Neos: 3 | Ui: 4 | resources: 5 | javascript: 6 | 'Sandstorm.LazyDataSource:Plugin': 7 | resource: resource://Sandstorm.LazyDataSource/Public/JavaScript/LazyDataSource/Plugin.js 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sandstorm.LazyDataSource 2 | 3 | [Inspector Data Sources](https://docs.neos.io/cms/manual/extending-neos-with-php-flow/custom-data-sources) 4 | in Neos are usually *eager*, this means the UI sends a single request to the backend to load all options, 5 | and then doing the filtering client-side. 6 | 7 | This package implements additional Inspector Editors, behaving like the standard SelectBoxEditor with 8 | data sources, but **delegates filtering and searching to the server-side**. 9 | 10 | This greatly improves Neos UI performance for data sources with big collections (100s of elements). 11 | 12 | ## Getting started 13 | 14 | 1. Default composer installation via: 15 | 16 | ```shell 17 | composer require sandstorm/lazydatasource 18 | ``` 19 | 20 | 2. **for a single select:** 21 | 22 | In your `NodeTypes.yaml`, activate the custom editor by using `Sandstorm.LazyDataSource/Inspector/Editors/DataSourceSelectEditor` 23 | instead of `Neos.Neos/Inspector/Editors/SelectBoxEditor`. **All configuration options [of the data source-based select](https://neos.readthedocs.io/en/stable/References/PropertyEditorReference.html#property-type-string-array-string-selectboxeditor-dropdown-select-editor) 24 | apply as usual. 25 | 26 | Additionally, we support the following additional `editorOptions`: 27 | 28 | - `dataSourceMakeNodeIndependent`: if set to TRUE, the currently selected node is NOT sent to the data source on the backend, 29 | increasing the cache lifetime on the client (e.g. the system can re-use elements from other nodes) 30 | 31 | **Example:** 32 | 33 | ```yaml 34 | 'Neos.Demo:Document.Page': 35 | properties: 36 | test: 37 | ui: 38 | inspector: 39 | group: 'document' 40 | 41 | ##### THIS IS THE RELEVANT CONFIG: 42 | editor: 'Sandstorm.LazyDataSource/Inspector/Editors/DataSourceSelectEditor' 43 | 44 | ##### all Select options (e.g. dataSourceAdditionalData) work as usual. 45 | editorOptions: 46 | placeholder: Choose 47 | dataSourceIdentifier: lazy-editor-test 48 | ``` 49 | 50 | 3. **for a multi select:** 51 | 52 | In your `NodeTypes.yaml`, activate the custom editor by using `Sandstorm.LazyDataSource/Inspector/Editors/DataSourceSelectEditor` 53 | instead of `Neos.Neos/Inspector/Editors/SelectBoxEditor`. **All configuration options [of the data source-based select](https://neos.readthedocs.io/en/stable/References/PropertyEditorReference.html#property-type-string-array-string-selectboxeditor-dropdown-select-editor) 54 | apply as usual. 55 | 56 | Additionally, we support the following additional `editorOptions`: 57 | 58 | - `dataSourceMakeNodeIndependent`: if set to TRUE, the currently selected node is NOT sent to the data source on the backend, 59 | increasing the cache lifetime on the client (e.g. the system can re-use elements from other nodes) 60 | 61 | **Example:** 62 | 63 | ```yaml 64 | 'Neos.Demo:Document.Page': 65 | properties: 66 | test2: 67 | 68 | ##### Do not forget to set the property type to array 69 | type: array 70 | ui: 71 | inspector: 72 | group: 'document' 73 | 74 | ##### THIS IS THE RELEVANT CONFIG: 75 | editor: 'Sandstorm.LazyDataSource/Inspector/Editors/DataSourceSelectEditor' 76 | 77 | ##### all Select options (e.g. dataSourceAdditionalData) work as usual. 78 | editorOptions: 79 | allowEmpty: true 80 | multiple: true 81 | placeholder: Choose 82 | dataSourceIdentifier: lazy-editor-test 83 | ``` 84 | 85 | **Do not forget to set the property type to `array`.** 86 | 87 | 4. In your `DataSource` implementation on the server, use the `LazyDataSourceTrait` and implement the two methods `getDataForIdentifiers()` 88 | and `searchData()`. Do not implement `getData()` (as this is provided by the trait). 89 | 90 | - `getDataForIdentifiers()` is called during the initial call, when the client-side needs to resolve entries for certain identifiers. 91 | - `searchData()` is called when the user has entered a search term, and needs to perform the searching. 92 | 93 | The return value for both methods needs to be the same as for normal data sources. 94 | 95 | Example: 96 | 97 | ```php 98 | use Neos\Neos\Service\DataSource\AbstractDataSource; 99 | use Neos\ContentRepository\Domain\Model\NodeInterface; 100 | 101 | class MyLazyDataSource extends AbstractDataSource 102 | { 103 | 104 | use LazyDataSourceTrait; 105 | 106 | static protected $identifier = 'lazy-editor-test'; 107 | 108 | protected function getDataForIdentifiers(array $identifiers, NodeInterface $node = null, array $arguments = []) 109 | { 110 | $options = []; 111 | foreach ($identifiers as $id) { 112 | $options[$id] = ['label' => 'My Label for ' . $id]; 113 | } 114 | return $options; 115 | } 116 | 117 | protected function searchData(string $searchTerm, NodeInterface $node = null, array $arguments = []) 118 | { 119 | $options = []; 120 | $options['key'] = ['label' => 'My Label ' . $searchTerm]; 121 | return $options; 122 | } 123 | } 124 | 125 | ``` 126 | 127 | ## Development 128 | 129 | This project works with yarn. The build process given by the neos developers is not very 130 | configurable, only the target dir for the buildprocess is adjustable by 131 | package.json. 132 | 133 | ```shell 134 | nvm install 135 | ``` 136 | 137 | If you don't have [yarn](https://yarnpkg.com/lang/en/docs/install/) already installed: 138 | 139 | ```shell 140 | brew install yarn 141 | ``` 142 | 143 | Build the app: 144 | 145 | ```shell 146 | ./dev.sh setup 147 | ./dev.sh build 148 | ``` 149 | 150 | ## Contribute 151 | 152 | You are very welcome to contribute by merge requests, adding issues etc. 153 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/LazyDataSource/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "", 3 | "license": "GNU GPLv3", 4 | "private": true, 5 | "scripts": { 6 | "build": "NODE_ENV=production neos-react-scripts build", 7 | "watch": "neos-react-scripts watch" 8 | }, 9 | "devDependencies": { 10 | "@neos-project/eslint-config-neos": "^2.3.0", 11 | "@neos-project/neos-ui-extensibility-webpack-adapter": "^9.0.0", 12 | "@neos-project/react-ui-components": "^9.0.0", 13 | "eslint": "^8.25.0" 14 | }, 15 | "main": "./src/index.js", 16 | "neos": { 17 | "buildTargetDirectory": "../../../Public/JavaScript/LazyDataSource" 18 | }, 19 | "dependencies": { 20 | "hashlru": "2.3.0" 21 | }, 22 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 23 | } 24 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/LazyDataSource/src/Editors/DataSourceSelectEditor.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import {SelectBox, MultiSelectBox} from '@neos-project/react-ui-components'; 5 | import {neos} from '@neos-project/neos-ui-decorators'; 6 | 7 | import dataLoader from './lazyDataSourceDataLoader'; 8 | import PreviewOption from './PreviewOption'; 9 | 10 | // COPIED FROM @neos-project/neos-ui-constants/src/dndTypes (as we do not expose this yet). 11 | const dndTypes = { 12 | NODE: 'neos-tree-node', 13 | MULTISELECT: 'neos-multiselect-value' 14 | }; 15 | 16 | @neos(globalRegistry => ({ 17 | i18nRegistry: globalRegistry.get('i18n') 18 | })) 19 | @dataLoader() 20 | export default class DataSourceSelectEditor extends PureComponent { 21 | static propTypes = { 22 | value: PropTypes.string, 23 | className: PropTypes.string, 24 | options: PropTypes.array, 25 | searchOptions: PropTypes.array, 26 | placeholder: PropTypes.string, 27 | displayLoadingIndicator: PropTypes.bool, 28 | threshold: PropTypes.number, 29 | onSearchTermChange: PropTypes.func, 30 | commit: PropTypes.func.isRequired, 31 | i18nRegistry: PropTypes.object.isRequired, 32 | disabled: PropTypes.bool, 33 | multiple: PropTypes.bool, 34 | }; 35 | 36 | handleValueChange = value => { 37 | this.props.commit(value); 38 | } 39 | 40 | render() { 41 | const { 42 | className, 43 | value, 44 | i18nRegistry, 45 | threshold, 46 | options, 47 | displayLoadingIndicator, 48 | onSearchTermChange, 49 | disabled, 50 | placeholder, 51 | searchOptions, 52 | multiple, 53 | } = this.props; 54 | 55 | return multiple ? : ; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/LazyDataSource/src/Editors/PreviewOption.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase, react/jsx-pascal-case */ 2 | import React, {PureComponent} from 'react'; 3 | import PropTypes from 'prop-types'; 4 | // eslint-disable-next-line camelcase 5 | import { SelectBox_Option_MultiLineWithThumbnail } from "@neos-project/react-ui-components"; 6 | 7 | export default class PreviewOption extends PureComponent { 8 | static propTypes = { 9 | option: PropTypes.shape({ 10 | label: PropTypes.string.isRequired, 11 | secondaryLabel: PropTypes.string, 12 | tertiaryLabel: PropTypes.string, 13 | icon: PropTypes.string, 14 | preview: PropTypes.string 15 | }) 16 | }; 17 | 18 | render() { 19 | const {option} = this.props; 20 | 21 | return ( 22 | 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/LazyDataSource/src/Editors/lazyDataSourceDataLoader.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | import {connect} from 'react-redux'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import {neos} from '@neos-project/neos-ui-decorators'; 6 | import {selectors} from '@neos-project/neos-ui-redux-store'; 7 | 8 | export default () => WrappedComponent => { 9 | @neos(globalRegistry => ({ 10 | lazyDataSourceDataLoader: globalRegistry.get('dataLoaders').get('SandstormLazyDataSourceLoader') 11 | })) 12 | @connect(state => ({ 13 | focusedNodePath: selectors.CR.Nodes.focusedNodePathSelector(state) 14 | })) 15 | class LazyDataSourceDataLoader extends PureComponent { 16 | static propTypes = { 17 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), 18 | options: PropTypes.shape({ 19 | multiple: PropTypes.bool, 20 | 21 | dataSourceIdentifier: PropTypes.string, 22 | dataSourceUri: PropTypes.string, 23 | dataSourceDisableCaching: PropTypes.bool, 24 | dataSourceAdditionalData: PropTypes.objectOf(PropTypes.any), 25 | 26 | // If dataSourceMakeNodeIndependent is TRUE, the dataLoader is not transmitting the currently selected node 27 | // to the backend; increasing the cache lifetime for the dataloaders in the client (e.g. the system can re-use 28 | // elements from other nodes) 29 | dataSourceMakeNodeIndependent: PropTypes.bool, 30 | }), 31 | 32 | lazyDataSourceDataLoader: PropTypes.shape({ 33 | resolveValue: PropTypes.func.isRequired, 34 | resolveValues: PropTypes.func.isRequired, 35 | search: PropTypes.func.isRequired 36 | }).isRequired, 37 | focusedNodePath: PropTypes.string, 38 | }; 39 | 40 | state = { 41 | isLoading: false, 42 | options: [], 43 | searchOptions: [], 44 | results: [] 45 | }; 46 | 47 | componentDidMount() { 48 | this.resolveValue(); 49 | } 50 | 51 | componentDidUpdate(prevProps) { 52 | if (prevProps.value !== this.props.value) { 53 | this.resolveValue(); 54 | } 55 | } 56 | 57 | resolveValue = () => { 58 | const { value, options, lazyDataSourceDataLoader } = this.props; 59 | const valueProvided = options.multiple ? Array.isArray(value) : value; 60 | if (valueProvided) { 61 | this.setState({isLoading: true}); 62 | const resolver = options.multiple ? lazyDataSourceDataLoader.resolveValues.bind(lazyDataSourceDataLoader) : lazyDataSourceDataLoader.resolveValue.bind(lazyDataSourceDataLoader); 63 | resolver(this.getDataLoaderOptions(), value) 64 | .then(options => { 65 | this.setState({ 66 | isLoading: false, 67 | options 68 | }); 69 | }); 70 | } 71 | } 72 | 73 | handleSearchTermChange = searchTerm => { 74 | if (searchTerm) { 75 | this.setState({isLoading: true, searchOptions: []}); 76 | this.props.lazyDataSourceDataLoader.search(this.getDataLoaderOptions(), searchTerm) 77 | .then(searchOptions => { 78 | this.setState({ 79 | isLoading: false, 80 | searchOptions 81 | }); 82 | }); 83 | } else { 84 | this.setState({ 85 | isLoading: false, 86 | searchOptions: [] 87 | }); 88 | } 89 | } 90 | 91 | getDataLoaderOptions() { 92 | return { 93 | contextNodePath: this.props.focusedNodePath, 94 | dataSourceIdentifier: this.props.options.dataSourceIdentifier, 95 | dataSourceUri: this.props.options.dataSourceUri, 96 | dataSourceAdditionalData: this.props.options.dataSourceAdditionalData, 97 | dataSourceDisableCaching: Boolean(this.props.options.dataSourceDisableCaching), 98 | dataSourceMakeNodeIndependent: Boolean(this.props.options.dataSourceMakeNodeIndependent) 99 | }; 100 | } 101 | 102 | render() { 103 | const { options, value } = this.props; 104 | const { isLoading, searchOptions } = this.state; 105 | 106 | const config = Object.assign({}, this.props, this.state); 107 | // For single-select, we want to display the search results as long as there are results 108 | const componentOptions = options.multiple ? this.state.options : ((isLoading || searchOptions.length) ? searchOptions : this.state.options); 109 | 110 | return ( 111 | 121 | ); 122 | } 123 | } 124 | return LazyDataSourceDataLoader; 125 | }; 126 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/LazyDataSource/src/index.js: -------------------------------------------------------------------------------- 1 | require('./manifest'); 2 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/LazyDataSource/src/manifest.js: -------------------------------------------------------------------------------- 1 | import HLRU from 'hashlru'; 2 | import manifest from '@neos-project/neos-ui-extensibility'; 3 | import backend from '@neos-project/neos-ui-backend-connector'; 4 | import DataSourceSelectEditor from './Editors/DataSourceSelectEditor'; 5 | 6 | function makeCacheKey(prefix, params) { 7 | if (params.options && params.options.dataSourceMakeNodeIndependent) { 8 | // if dataSourceMakeNodeIndependent, remove contextNodePath from the cache key. 9 | params = JSON.parse(JSON.stringify(params)); // Deep copy 10 | delete params.options.contextNodePath; 11 | } 12 | return prefix + JSON.stringify(params); 13 | } 14 | 15 | manifest('Sandstorm.LazyDataSource:Plugin', {}, (globalRegistry) => { 16 | 17 | const editorsRegistry = globalRegistry.get('inspector').get('editors'); 18 | 19 | editorsRegistry.set('Sandstorm.LazyDataSource/Inspector/Editors/DataSourceSelectEditor', { 20 | component: DataSourceSelectEditor 21 | }); 22 | 23 | const dataLoadersRegistry = globalRegistry.get('dataLoaders'); 24 | dataLoadersRegistry.set('SandstormLazyDataSourceLoader', { 25 | description: ` 26 | Look up Data Source values: 27 | 28 | - by identifier (resolveValue()) 29 | - by searching in data source (search()) 30 | 31 | OPTIONS: 32 | - contextNodePath: ... 33 | - dataSourceIdentifier: The data source to load. Either this or dataSourceUri is required. 34 | - dataSourceUri: The data source URL to load. 35 | - dataSourceDisableCaching: Disable default _lru caching option. 36 | - dataSourceAdditionalData: Additional data to send to the server 37 | 38 | EXTRA OPTIONS: 39 | - dataSourceMakeNodeIndependent: If set to TRUE, the dataLoader is not transmitting the currently selected node 40 | to the backend; increasing the cache lifetime for the dataloaders in the client (e.g. the system can re-use 41 | elements from other nodes) 42 | `, 43 | 44 | _lru() { 45 | if (!this._lruCache) { 46 | this._lruCache = new HLRU(500); 47 | } 48 | return this._lruCache; 49 | }, 50 | 51 | resolveValue(options, identifier) { 52 | return this.resolveValues(options, [identifier]); 53 | }, 54 | 55 | // modelled as a combination of DataSources and NodeLookup data loaders. The rough 56 | // structure is the NodeLookup data loader; but the querying part is the DataSources data loader. 57 | resolveValues(options, identifiers) { 58 | const resultPromisesByIdentifier = {}; 59 | const identifiersNotInCache = []; 60 | 61 | identifiers.forEach(identifier => { 62 | const cacheKey = makeCacheKey('resolve', {options, identifier}); 63 | 64 | if (this._lru().has(cacheKey)) { 65 | resultPromisesByIdentifier[identifier] = this._lru().get(cacheKey); 66 | } else { 67 | identifiersNotInCache.push(identifier); 68 | } 69 | }); 70 | 71 | let result; 72 | if (identifiersNotInCache.length > 0) { 73 | // Build up query 74 | const params = Object.assign(options.dataSourceMakeNodeIndependent ? {} : {node: options.contextNodePath}, options.dataSourceAdditionalData || {}, { 75 | identifiers: identifiersNotInCache 76 | }); 77 | // Trigger query 78 | const dataSourceApi = backend.get().endpoints.dataSource; 79 | result = dataSourceApi(options.dataSourceIdentifier, options.dataSourceUri, params).then(results => { 80 | const resultsAsArray = Object.keys(results).map(identifier => ({ 81 | ...(results[identifier]), 82 | value: identifier 83 | })); 84 | 85 | // We store the result in the cache 86 | resultsAsArray.forEach(result => { 87 | const cacheKey = makeCacheKey('resolve', {options, identifier: result.value}); 88 | const resultPromise = Promise.resolve(result); 89 | if (!options.dataSourceDisableCaching) { 90 | this._lru().set(cacheKey, resultPromise); 91 | } 92 | resultPromisesByIdentifier[result.value] = resultPromise; 93 | }); 94 | 95 | // By now, all identifiers are in cache. 96 | return Promise.all( 97 | identifiers.map(identifier => 98 | resultPromisesByIdentifier[identifier] 99 | ).filter(promise => Boolean(promise)) // Remove "null" values 100 | ); 101 | }); 102 | } else { 103 | // We know all identifiers are in cache. 104 | result = Promise.all( 105 | identifiers.map(identifier => 106 | resultPromisesByIdentifier[identifier] 107 | ).filter(promise => Boolean(promise)) // Remove "null" values 108 | ); 109 | } 110 | 111 | return result; 112 | }, 113 | 114 | search(options, searchTerm) { 115 | if (!searchTerm) { 116 | return Promise.resolve([]); 117 | } 118 | 119 | const cacheKey = makeCacheKey('search', {options, searchTerm}); 120 | if (this._lru().has(cacheKey)) { 121 | return this._lru().get(cacheKey); 122 | } 123 | 124 | // Debounce AJAX requests for 300 ms 125 | return new Promise(resolve => { 126 | if (this._debounceTimer) { 127 | window.clearTimeout(this._debounceTimer); 128 | } 129 | this._debounceTimer = window.setTimeout(resolve, 300); 130 | }).then(() => { 131 | // Build up query 132 | const searchQuery = Object.assign(options.dataSourceMakeNodeIndependent ? {} : {node: options.contextNodePath}, options.dataSourceAdditionalData || {}, { 133 | searchTerm 134 | }); 135 | 136 | // Trigger query 137 | const dataSourceApi = backend.get().endpoints.dataSource; 138 | const resultPromise = dataSourceApi(options.dataSourceIdentifier, options.dataSourceUri, searchQuery).then(results => { 139 | return Object.keys(results).map(identifier => ({ 140 | ...(results[identifier]), 141 | value: identifier 142 | })); 143 | }); 144 | if (!options.dataSourceDisableCaching) { 145 | this._lru().set(cacheKey, resultPromise); 146 | } 147 | 148 | // Next to storing the full result in the cache, we also store each individual result in the cache; 149 | // in the same format as expected by resolveValue(); so that it is already loaded and does not need 150 | // to be loaded once the element has been selected. 151 | return resultPromise.then(results => { 152 | results.forEach(result => { 153 | const cacheKey = makeCacheKey('resolve', {options, identifier: result.value}); 154 | if (!options.dataSourceDisableCaching) { 155 | this._lru().set(cacheKey, Promise.resolve(result)); 156 | } 157 | }); 158 | 159 | return results; 160 | }); 161 | }); 162 | } 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /Resources/Public/JavaScript/LazyDataSource/Plugin.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function r(a){if(t[a])return t[a].exports;var n=t[a]={i:a,l:!1,exports:{}};return e[a].call(n.exports,n,n.exports,r),n.l=!0,n.exports}r.m=e,r.c=t,r.d=function(e,t,a){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:a})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var a=Object.create(null);if(r.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)r.d(a,n,function(t){return e[t]}.bind(null,n));return a},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=6)}([function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){return function(){var t;if(window["@Neos:HostPluginAPI"]&&window["@Neos:HostPluginAPI"]["@"+e])return(t=window["@Neos:HostPluginAPI"])["@"+e].apply(t,arguments);throw new Error("You are trying to read from a consumer api that hasn't been initialized yet!")}}},function(e,t,r){"use strict";var a,n=r(0),o=(a=n)&&a.__esModule?a:{default:a};e.exports=(0,o.default)("vendor")().React},function(e,t,r){"use strict";var a,n=r(0),o=(a=n)&&a.__esModule?a:{default:a};e.exports=(0,o.default)("vendor")().PropTypes},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=o(r(13)),n=o(r(14));function o(e){return e&&e.__esModule?e:{default:e}}var i=class extends a.default{constructor(e){super(e),this._registry=[]}set(e,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;if("string"!=typeof e)throw new Error("Key must be a string");if("string"!=typeof r&&"number"!=typeof r)throw new Error("Position must be a string or a number");var a={key:e,value:t};r&&(a.position=r);var n=this._registry.findIndex((function(t){return t.key===e}));return-1===n?this._registry.push(a):this._registry[n]=a,t}get(e){if("string"!=typeof e)return console.error("Key must be a string"),null;var t=this._registry.find((function(t){return t.key===e}));return t?t.value:null}_getChildrenWrapped(e){var t=this._registry.filter((function(t){return 0===t.key.indexOf(e+"/")}));return(0,n.default)(t)}getChildrenAsObject(e){var t={};return this._getChildrenWrapped(e).forEach((function(e){t[e.key]=e.value})),t}getChildren(e){return this._getChildrenWrapped(e).map((function(e){return e.value}))}has(e){return"string"!=typeof e?(console.error("Key must be a string"),!1):Boolean(this._registry.find((function(t){return t.key===e})))}_getAllWrapped(){return(0,n.default)(this._registry)}getAllAsObject(){var e={};return this._getAllWrapped().forEach((function(t){e[t.key]=t.value})),e}getAllAsList(){return this._getAllWrapped().map((function(e){return Object.assign({id:e.key},e.value)}))}};t.default=i},function(e,t,r){"use strict";var a,n=r(0),o=(a=n)&&a.__esModule?a:{default:a};e.exports=(0,o.default)("NeosProjectPackages")().ReactUiComponents},function(e,t,r){"use strict";var a,n=r(0),o=(a=n)&&a.__esModule?a:{default:a};e.exports=(0,o.default)("NeosProjectPackages")().NeosUiDecorators},function(e,t,r){"use strict";r(7)},function(e,t,r){"use strict";var a=Object.assign||function(e){for(var t=1;t0){var s=Object.assign(e.dataSourceMakeNodeIndependent?{}:{node:e.contextNodePath},e.dataSourceAdditionalData||{},{identifiers:o});u=(0,i.default.get().endpoints.dataSource)(e.dataSourceIdentifier,e.dataSourceUri,s).then((function(o){return Object.keys(o).map((function(e){return a({},o[e],{value:e})})).forEach((function(t){var a=l("resolve",{options:e,identifier:t.value}),o=Promise.resolve(t);e.dataSourceDisableCaching||r._lru().set(a,o),n[t.value]=o})),Promise.all(t.map((function(e){return n[e]})).filter((function(e){return Boolean(e)})))}))}else u=Promise.all(t.map((function(e){return n[e]})).filter((function(e){return Boolean(e)})));return u},search:function(e,t){var r=this;if(!t)return Promise.resolve([]);var n=l("search",{options:e,searchTerm:t});return this._lru().has(n)?this._lru().get(n):new Promise((function(e){r._debounceTimer&&window.clearTimeout(r._debounceTimer),r._debounceTimer=window.setTimeout(e,300)})).then((function(){var o=Object.assign(e.dataSourceMakeNodeIndependent?{}:{node:e.contextNodePath},e.dataSourceAdditionalData||{},{searchTerm:t}),u=(0,i.default.get().endpoints.dataSource)(e.dataSourceIdentifier,e.dataSourceUri,o).then((function(e){return Object.keys(e).map((function(t){return a({},e[t],{value:t})}))}));return e.dataSourceDisableCaching||r._lru().set(n,u),u.then((function(t){return t.forEach((function(t){var a=l("resolve",{options:e,identifier:t.value});e.dataSourceDisableCaching||r._lru().set(a,Promise.resolve(t))})),t}))}))}})}))},function(e,t){e.exports=function(e){if(!e)throw Error("hashlru must have a max value, of type number, greater than 0");var t=0,r=Object.create(null),a=Object.create(null);function n(n,o){r[n]=o,++t>=e&&(t=0,a=r,r=Object.create(null))}return{has:function(e){return void 0!==r[e]||void 0!==a[e]},remove:function(e){void 0!==r[e]&&(r[e]=void 0),void 0!==a[e]&&(a[e]=void 0)},get:function(e){var t=r[e];return void 0!==t?t:void 0!==(t=a[e])?(n(e,t),t):void 0},set:function(e,t){void 0!==r[e]?r[e]=t:n(e,t)},clear:function(){r=Object.create(null),a=Object.create(null)}}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.SynchronousMetaRegistry=t.SynchronousRegistry=t.readFromConsumerApi=t.createConsumerApi=void 0;var a=i(r(10)),n=i(r(0)),o=r(12);function i(e){return e&&e.__esModule?e:{default:e}}t.default=(0,n.default)("manifest"),t.createConsumerApi=a.default,t.readFromConsumerApi=n.default,t.SynchronousRegistry=o.SynchronousRegistry,t.SynchronousMetaRegistry=o.SynchronousMetaRegistry},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t){var r={};Object.keys(t).forEach((function(e){Object.defineProperty(r,e,i(t[e]))})),Object.defineProperty(r,"@manifest",i((0,o.default)(e))),Object.defineProperty(window,"@Neos:HostPluginAPI",i(r))};var a,n=r(11),o=(a=n)&&a.__esModule?a:{default:a};var i=function(e){return{value:e,writable:!1,enumerable:!1,configurable:!0}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){return function(t,r,a){var n,o,i;e.push((i={options:r,bootstrap:a},(o=t)in(n={})?Object.defineProperty(n,o,{value:i,enumerable:!0,configurable:!0,writable:!0}):n[o]=i,n))}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.SynchronousMetaRegistry=t.SynchronousRegistry=void 0;var a=o(r(3)),n=o(r(15));function o(e){return e&&e.__esModule?e:{default:e}}t.SynchronousRegistry=a.default,t.SynchronousMetaRegistry=n.default},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});t.default=class{constructor(e){this.SERIAL_VERSION_UID="d8a5aa78-978e-11e6-ae22-56b6b6499611",this.description=e}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});t.default=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"position",r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"key",a="string"==typeof t?function(e){return e[t]}:t,n={},o={},i={},u={},s={},l={};e.forEach((function(e,t){var d=e[r]?e[r]:String(t);n[d]=t;var c=a(e),f=String(c||t),h=!1;if(f.startsWith("start")){var p=f.match(/start\s+(\d+)/),v=p&&p[1]?Number(p[1]):0;i[v]||(i[v]=[]),i[v].push(d)}else if(f.startsWith("end")){var y=f.match(/end\s+(\d+)/),g=y&&y[1]?Number(y[1]):0;u[g]||(u[g]=[]),u[g].push(d)}else if(f.startsWith("before")){var b=f.match(/before\s+(\S+)(\s+(\d+))?/);if(b){var m=b[1],S=b[3]?Number(b[3]):0;s[m]||(s[m]={}),s[m][S]||(s[m][S]=[]),s[m][S].push(d)}else h=!0}else if(f.startsWith("after")){var _=f.match(/after\s+(\S+)(\s+(\d+))?/);if(_){var O=_[1],P=_[3]?Number(_[3]):0;l[O]||(l[O]={}),l[O][P]||(l[O][P]=[]),l[O][P].push(d)}else h=!0}else h=!0;if(h){var N=parseFloat(f);!isNaN(N)&&isFinite(N)||(N=t),o[N]||(o[N]=[]),o[N].push(d)}}));var d=[],c=[],f=[],h=[],p=function(e,t){var r=Object.keys(e).map((function(e){return Number(e)})).sort((function(e,t){return e-t}));return t?r:r.reverse()},v=function e(t,r){t.forEach((function(t){if(!(h.indexOf(t)>=0)){if(h.push(t),s[t]){var a=p(s[t],!0),n=!0,o=!1,i=void 0;try{for(var u,d=a[Symbol.iterator]();!(n=(u=d.next()).done);n=!0){var c=u.value;e(s[t][c],r)}}catch(e){o=!0,i=e}finally{try{!n&&d.return&&d.return()}finally{if(o)throw i}}}if(r.push(t),l[t]){var f=p(l[t],!1),v=!0,y=!1,g=void 0;try{for(var b,m=f[Symbol.iterator]();!(v=(b=m.next()).done);v=!0){var S=b.value;e(l[t][S],r)}}catch(e){y=!0,g=e}finally{try{!v&&m.return&&m.return()}finally{if(y)throw g}}}}}))},y=!0,g=!1,b=void 0;try{for(var m,S=p(i,!1)[Symbol.iterator]();!(y=(m=S.next()).done);y=!0){var _=m.value;v(i[_],d)}}catch(e){g=!0,b=e}finally{try{!y&&S.return&&S.return()}finally{if(g)throw b}}var O=!0,P=!1,N=void 0;try{for(var M,j=p(o,!0)[Symbol.iterator]();!(O=(M=j.next()).done);O=!0){var L=M.value;v(o[L],c)}}catch(e){P=!0,N=e}finally{try{!O&&j.return&&j.return()}finally{if(P)throw N}}var w=!0,x=!1,D=void 0;try{for(var T,C=p(u,!0)[Symbol.iterator]();!(w=(T=C.next()).done);w=!0){var E=T.value;v(u[E],f)}}catch(e){x=!0,D=e}finally{try{!w&&C.return&&C.return()}finally{if(x)throw D}}var I=!0,R=!1,k=void 0;try{for(var A,U=Object.keys(s)[Symbol.iterator]();!(I=(A=U.next()).done);I=!0){var V=A.value;if(!(h.indexOf(V)>=0)){var B=!0,W=!1,z=void 0;try{for(var q,F=p(s[V],!1)[Symbol.iterator]();!(B=(q=F.next()).done);B=!0){var H=q.value;v(s[V][H],d)}}catch(e){W=!0,z=e}finally{try{!B&&F.return&&F.return()}finally{if(W)throw z}}}}}catch(e){R=!0,k=e}finally{try{!I&&U.return&&U.return()}finally{if(R)throw k}}var J=!0,K=!1,Y=void 0;try{for(var X,G=Object.keys(l)[Symbol.iterator]();!(J=(X=G.next()).done);J=!0){var Q=X.value;if(!(h.indexOf(Q)>=0)){var Z=!0,$=!1,ee=void 0;try{for(var te,re=p(l[Q],!1)[Symbol.iterator]();!(Z=(te=re.next()).done);Z=!0){var ae=te.value;v(l[Q][ae],c)}}catch(e){$=!0,ee=e}finally{try{!Z&&re.return&&re.return()}finally{if($)throw ee}}}}}catch(e){K=!0,Y=e}finally{try{!J&&G.return&&G.return()}finally{if(K)throw Y}}var ne=[].concat(d,c,f);return ne.map((function(e){return n[e]})).map((function(t){return e[t]}))}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a,n=r(3),o=(a=n)&&a.__esModule?a:{default:a};var i=class extends o.default{set(e,t){if("d8a5aa78-978e-11e6-ae22-56b6b6499611"!==t.SERIAL_VERSION_UID)throw new Error("You can only add registries to a meta registry");return super.set(e,t)}};t.default=i},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.fetchWithErrorHandling=void 0;var a,n=r(0),o=(a=n)&&a.__esModule?a:{default:a};t.default=(0,o.default)("NeosProjectPackages")().NeosUiBackendConnectorDefault;var i=(0,o.default)("NeosProjectPackages")().NeosUiBackendConnector.fetchWithErrorHandling;t.fetchWithErrorHandling=i},function(e,t,r){"use strict";var a,n,o;Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=r(1),u=h(i),s=h(r(2)),l=r(4),d=r(5),c=h(r(18)),f=h(r(21));function h(e){return e&&e.__esModule?e:{default:e}}var p="neos-multiselect-value",v=(0,d.neos)((function(e){return{i18nRegistry:e.get("i18n")}}))(a=(0,c.default)()((o=n=class extends i.PureComponent{constructor(){var e,t;return e=t=super(...arguments),this.handleValueChange=function(e){t.props.commit(e)},e}render(){var e=this.props,t=e.className,r=e.value,a=e.i18nRegistry,n=e.threshold,o=e.options,i=e.displayLoadingIndicator,s=e.onSearchTermChange,d=e.disabled,c=e.placeholder,h=e.searchOptions;return e.multiple?u.default.createElement(l.MultiSelectBox,{className:t,displaySearchBox:!0,noMatchesFoundLabel:a.translate("Neos.Neos:Main:noMatchesFound"),loadingLabel:a.translate("Neos.Neos:Main:loading"),searchBoxLeftToTypeLabel:a.translate("Neos.Neos:Main:searchBoxLeftToType"),placeholder:a.translate(c),threshold:n,options:o,values:r,onValuesChange:this.handleValueChange,displayLoadingIndicator:i,showDropDownToggle:!1,allowEmpty:!0,onSearchTermChange:s,ListPreviewElement:f.default,disabled:d,searchOptions:h,dndType:p}):u.default.createElement(l.SelectBox,{className:t,displaySearchBox:!0,noMatchesFoundLabel:a.translate("Neos.Neos:Main:noMatchesFound"),loadingLabel:a.translate("Neos.Neos:Main:loading"),searchBoxLeftToTypeLabel:a.translate("Neos.Neos:Main:searchBoxLeftToType"),placeholder:a.translate(c),threshold:n,options:o,value:r,onValueChange:this.handleValueChange,displayLoadingIndicator:i,showDropDownToggle:!1,allowEmpty:!0,onSearchTermChange:s,ListPreviewElement:f.default,disabled:d})}},n.propTypes={value:s.default.string,className:s.default.string,options:s.default.array,searchOptions:s.default.array,placeholder:s.default.string,displayLoadingIndicator:s.default.bool,threshold:s.default.number,onSearchTermChange:s.default.func,commit:s.default.func.isRequired,i18nRegistry:s.default.object.isRequired,disabled:s.default.bool,multiple:s.default.bool},a=o))||a)||a;t.default=v},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var a=Object.assign||function(e){for(var t=1;t