diff --git a/src/Field/WfsSearch/WfsSearch.example.md b/src/Field/WfsSearch/WfsSearch.example.md deleted file mode 100644 index 1c76e7909f..0000000000 --- a/src/Field/WfsSearch/WfsSearch.example.md +++ /dev/null @@ -1,72 +0,0 @@ -This demonstrates the usage of the WfsSearch. -Type a country name in its own language… - -```jsx -import WfsSearch from '@terrestris/react-geo/dist/Field/WfsSearch/WfsSearch'; -import MapComponent from '@terrestris/react-util/dist/Components/MapComponent/MapComponent'; -import MapContext from '@terrestris/react-util/dist/Context/MapContext/MapContext'; -import OlLayerTile from 'ol/layer/Tile'; -import OlMap from 'ol/Map'; -import { fromLonLat } from 'ol/proj'; -import OlSourceOSM from 'ol/source/OSM'; -import OlView from 'ol/View'; -import {useEffect, useState} from 'react'; - -const WfsSearchExample = () => { - - const [map, setMap] = useState(); - - useEffect(() => { - const newMap = new OlMap({ - layers: [ - new OlLayerTile({ - name: 'OSM', - source: new OlSourceOSM() - }) - ], - view: new OlView({ - center: fromLonLat([37.40570, 8.81566]), - zoom: 4 - }) - }); - - setMap(newMap); - }, []); - - if (!map) { - return null; - } - - return ( - - - WFS Search: - - - - - - ); -}; - -``` diff --git a/src/Field/WfsSearch/WfsSearch.spec.tsx b/src/Field/WfsSearch/WfsSearch.spec.tsx deleted file mode 100644 index 67b80126f5..0000000000 --- a/src/Field/WfsSearch/WfsSearch.spec.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import Logger from '@terrestris/base-util/dist/Logger'; -import {OptionProps} from 'antd/lib/select'; -import OlLayerTile from 'ol/layer/Tile'; -import OlMap from 'ol/Map'; -import OlSourceOsm from 'ol/source/OSM'; -import OlView from 'ol/View'; -import { - act -} from 'react-dom/test-utils'; - -import TestUtil from '../../Util/TestUtil'; -import WfsSearch, {WfsSearchProps, WfsSearchState} from './WfsSearch'; - -describe('', () => { - it('is defined', () => { - expect(WfsSearch).not.toBeUndefined(); - }); - - it('can be rendered', () => { - const wrapper = TestUtil.mountComponent(WfsSearch); - expect(wrapper).not.toBeUndefined(); - }); - - describe('#onUpdateInput', () => { - - it('sets the inputValue as state.searchTerm', () => { - const wrapper = TestUtil.mountComponent(WfsSearch); - const instance = wrapper.instance() as WfsSearch; - - const inputValue = 'a'; - act(() => { - instance.onUpdateInput(inputValue); - }); - let state = wrapper.state() as WfsSearchState; - expect(state.searchTerm).toBe(inputValue); - }); - - it('sends a request if input is as long as props.minChars', () => { - // expect.assertions(1); - const wrapper = TestUtil.mountComponent(WfsSearch, { - placeholder: 'Type a countryname in its own language…', - baseUrl: 'https://ows-demo.terrestris.de/geoserver/osm/wfs', - featureTypes: ['osm:osm-country-borders'], - attributeDetails: { - 'osm:osm-country-borders': { - name: { - type: 'string', - exactSearch: false, - matchCase: false - } - } - } - }); - const instance = wrapper.instance() as WfsSearch; - const doSearchSpy = jest.spyOn(instance, 'doSearch'); - const inputValue = 'Deutsch'; - act(() => { - instance.onUpdateInput(inputValue); - }); - expect(doSearchSpy).toHaveBeenCalled(); - doSearchSpy.mockRestore(); - }); - }); - - describe('#onFetchSuccess', () => { - it('sets the response features as state.data', () => { - const wrapper = TestUtil.mountComponent(WfsSearch); - const response = { - features: [{ - id: '752526', - properties: { - name: 'Deutschland' - } - }] - }; - const instance = wrapper.instance() as WfsSearch; - instance.onFetchSuccess(response); - const promise = new Promise(resolve => { - setTimeout(resolve, 350); - }); - return promise.then(() => { - expect((wrapper.state() as WfsSearchState).data).toEqual(response.features); - }); - }); - }); - - describe('#onFetchError', () => { - it('sets the response as state.data', () => { - const wrapper = TestUtil.mountComponent(WfsSearch); - const loggerSpy = jest.spyOn(Logger, 'error'); - const instance = wrapper.instance() as WfsSearch; - instance.onFetchError('Peter'); - expect(loggerSpy).toHaveBeenCalled(); - expect(loggerSpy).toHaveBeenCalledWith('Error while requesting WFS GetFeature: Peter'); - loggerSpy.mockRestore(); - }); - }); - - describe('#onMenuItemSelected', () => { - it('calls this.props.onSelect with the selected item', () => { - // SETUP - const data = [{ - id: '752526', - properties: { - name: 'Deutschland' - } - }]; - const map = new OlMap({ - layers: [new OlLayerTile({ source: new OlSourceOsm(), properties: {name: 'OSM'} })], - view: new OlView({ - projection: 'EPSG:4326', - center: [37.40570, 8.81566], - zoom: 4 - }) - }); - // SETUP END - - const selectSpy = jest.fn(); - const wrapper = TestUtil.mountComponent(WfsSearch, { - onSelect: selectSpy, - map - }); - const instance = wrapper.instance() as WfsSearch; - act(() => { - wrapper.setState({ - data: data - }); - }); - act(() => { - const op: OptionProps = { key: '752526', children: null }; - instance.onMenuItemSelected('Deutschland', op); - }); - expect(selectSpy).toHaveBeenCalled(); - expect(selectSpy).toHaveBeenCalledWith(data[0], map); - - selectSpy.mockRestore(); - }); - }); - - describe('default #onSelect', () => { - it('zooms to the selected feature', () => { - // SETUP - const feature = { - type: 'Feature', - id: '752526', - properties: { - name: 'Peter', - }, - geometry: { - type: 'Polygon', - coordinates: [[[10, 40], [40, 40], [40, 10], [10, 10], [10, 40]]] - } - }; - const map = new OlMap({ - layers: [new OlLayerTile({ source: new OlSourceOsm(), properties: {name: 'OSM'} })], - view: new OlView({ - projection: 'EPSG:4326', - center: [37.40570, 8.81566], - zoom: 4, - constrainResolution: true - }) - }); - // SETUP END - - const wrapper = TestUtil.mountComponent(WfsSearch, { map }); - const fitSpy = jest.spyOn(map.getView(), 'fit'); - (wrapper.props() as WfsSearchProps).onSelect(feature, map); - - expect.assertions(3); - - expect(fitSpy).toHaveBeenCalled(); - - return new Promise(resolve => { - setTimeout(resolve, 600); - }) - .then(() => { - expect(map.getView().getCenter()).toEqual([25, 25]); - expect(map.getView().getZoom()).toEqual(2); - fitSpy.mockRestore(); - }); - }); - }); - - describe('#renderOption', () => { - it('returns a Select.Option', () => { - const wrapper = TestUtil.mountComponent(WfsSearch); - const feature = { - id: '752526', - properties: { - name: 'Deutschland' - } - }; - const option = (wrapper.props() as WfsSearchProps).renderOption(feature, { - // Props must be passed to the renderOption function. - displayValue: 'name', - idProperty: 'id' - }); - - expect(option.key).toBe(feature.id); - expect(option.props.children).toBe(feature.properties.name); - }); - }); - - describe('#idProperty', () => { - it('can be specified', () => { - const wrapper = TestUtil.mountComponent(WfsSearch); - const feature = { - customId: '7355608', - properties: { - name: 'Deutschland' - } - }; - const option = (wrapper.props() as WfsSearchProps).renderOption(feature, { - displayValue: 'name', - idProperty: 'customId' - }); - - expect(option.key).toBe(feature.customId); - expect(option.props.children).toBe(feature.properties.name); - }); - }); - -}); diff --git a/src/Field/WfsSearchField/WfsSearchField.example.md b/src/Field/WfsSearchField/WfsSearchField.example.md new file mode 100644 index 0000000000..89efed03a7 --- /dev/null +++ b/src/Field/WfsSearchField/WfsSearchField.example.md @@ -0,0 +1,121 @@ +This demonstrates the usage of the WfsSearch. +Type a country name in its own language… + +```jsx +import WfsSearch from '@terrestris/react-geo/dist/Field/WfsSearchField/WfsSearchField'; +import MapComponent from '@terrestris/react-util/dist/Components/MapComponent/MapComponent'; +import MapContext from '@terrestris/react-util/dist/Context/MapContext/MapContext'; +import AgFeatureGrid from '@terrestris/react-geo/dist/Grid/AgFeatureGrid/AgFeatureGrid'; +import OlLayerTile from 'ol/layer/Tile'; +import OlMap from 'ol/Map'; +import { fromLonLat } from 'ol/proj'; +import OlSourceOSM from 'ol/source/OSM'; +import OlView from 'ol/View'; +import {useEffect, useState} from 'react'; + +const WfsSearchFieldExample = () => { + + const [map, setMap] = useState(); + const [inputFeatures, setInputFeatures] = useState([]); + + useEffect(() => { + const newMap = new OlMap({ + layers: [ + new OlLayerTile({ + name: 'OSM', + source: new OlSourceOSM() + }) + ], + view: new OlView({ + center: fromLonLat([37.40570, 8.81566]), + zoom: 4 + }) + }); + + setMap(newMap); + }, []); + + if (!map) { + return null; + } + + const onFeaturesChange = f => setInputFeatures(f); + + return ( + + + WFS Search als AutoComplete (default): + + + + + WFS Search as Input + + { + inputFeatures && inputFeatures.length > 0 && + + } + + + + + ); +}; + +``` diff --git a/src/Field/WfsSearch/WfsSearch.less b/src/Field/WfsSearchField/WfsSearchField.less similarity index 100% rename from src/Field/WfsSearch/WfsSearch.less rename to src/Field/WfsSearchField/WfsSearchField.less diff --git a/src/Field/WfsSearchField/WfsSearchField.spec.tsx b/src/Field/WfsSearchField/WfsSearchField.spec.tsx new file mode 100644 index 0000000000..40143d5103 --- /dev/null +++ b/src/Field/WfsSearchField/WfsSearchField.spec.tsx @@ -0,0 +1,14 @@ +import TestUtil from '../../Util/TestUtil'; +import WfsSearchField from './WfsSearchField'; + +describe('', () => { + it('is defined', () => { + expect(WfsSearchField).not.toBeUndefined(); + }); + + it('can be rendered', () => { + const wrapper = TestUtil.mountComponent(WfsSearchField); + expect(wrapper).not.toBeUndefined(); + }); + +}); diff --git a/src/Field/WfsSearch/WfsSearch.tsx b/src/Field/WfsSearchField/WfsSearchField.tsx similarity index 61% rename from src/Field/WfsSearch/WfsSearch.tsx rename to src/Field/WfsSearchField/WfsSearchField.tsx index ca17688819..103f68b81f 100644 --- a/src/Field/WfsSearch/WfsSearch.tsx +++ b/src/Field/WfsSearchField/WfsSearchField.tsx @@ -1,51 +1,50 @@ -import './WfsSearch.less'; +import './WfsSearchField.less'; +import { faCircleNotch, faClose } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import Logger from '@terrestris/base-util/dist/Logger'; import { SearchConfig } from '@terrestris/ol-util/dist/WfsFilterUtil/WfsFilterUtil'; import useMap from '@terrestris/react-util/dist/Hooks/useMap/useMap'; import { useWfs } from '@terrestris/react-util/dist/Hooks/useWfs/useWfs'; -import { AutoComplete, Spin } from 'antd'; -import { AutoCompleteProps } from 'antd/lib/auto-complete'; +import { AutoComplete, Input, Spin } from 'antd'; import { DefaultOptionType, OptionProps } from 'antd/lib/select'; import _get from 'lodash/get'; import _isNil from 'lodash/isNil'; +import _isString from 'lodash/isString'; import OlFeature from 'ol/Feature'; -import React, { FC, useMemo, useState } from 'react'; +import React, { FC, useEffect, useMemo, useState } from 'react'; import { CSS_PREFIX } from '../../constants'; const Option = AutoComplete.Option; -export interface WfsSearchState { - data: Array; - fetching: boolean; - searchTerm: string; -} - -export type WfsSearchProps = { +export type WfsSearchFieldProps = { additionalFetchOptions?: Partial; + asInput?: boolean; baseUrl: string; className?: string; displayValue?: string; idProperty?: string; minChars?: number; -} & SearchConfig & AutoCompleteProps ; + onBeforeSearch?: (value: string) => string; + onChange?: (val: OlFeature[] | undefined) => undefined; + value?: OlFeature[] | undefined; +} & SearchConfig; const defaultClassName = `${CSS_PREFIX}wfssearch`; /** - * The WfsSearch field. - * Implements an input field to do a WFS-GetFeature request. + * The WfsSearchField field. + * Implements a field to do a WFS-GetFeature request. * * The GetFeature request is created with `ol.format.WFS.writeGetFeature` * so most of the WFS specific options work like document in the corresponding * API-docs: https://openlayers.org/en/latest/apidoc/module-ol_format_WFS.html * - * @class WfsSearch - * @extends React.Component */ -export const WfsSearch: FC = ({ +export const WfsSearchField: FC = ({ additionalFetchOptions, + asInput = false, attributeDetails = {}, baseUrl, className, @@ -57,9 +56,12 @@ export const WfsSearch: FC = ({ idProperty = 'id', maxFeatures, minChars = 3, + onBeforeSearch = v => v, + onChange = () => undefined, outputFormat = 'application/json', propertyNames, srsName = 'EPSG:3857', + value, wfsFormatOptions, ...passThroughProps }) => { @@ -114,19 +116,19 @@ export const WfsSearch: FC = ({ }); /** - * Create an AutoComplete.Option from the given data. - * - * @param feature The feature as returned by the server. - * @return The AutoComplete.Option that will be - * rendered for each feature. - */ + * Create an AutoComplete.Option from the given data. + * + * @param feature The feature as returned by the server. + * @return The AutoComplete.Option that will be + * rendered for each feature. + */ const renderOption = (feature?: OlFeature): React.ReactElement => { if (_isNil(feature)) { return <>>; } - const value = _get(feature?.getProperties(), displayValue); - const display = _isNil(value) ? feature.get(idProperty) : value; + const v = _get(feature?.getProperties(), displayValue); + const display = _isNil(v) ? feature.get(idProperty) : v; return ( = ({ } }; - const onUpdateInput = (val: string) => setSearchTerm(val); + /** + * Called if the input is being updated. It sets the current inputValue + * as searchTerm and starts a search if the inputValue has + * a length of at least `this.props.minChars` (default 3). + * + * Gets undefined if clear btn is pressed. + * @param val + */ + const onUpdateInput = (val: any) => { + let updatedValue = ''; + if (_isString(val)) { + updatedValue = onBeforeSearch(val); + } else { + if (val?.target?.value) { + updatedValue = onBeforeSearch(val.target.value); + } + } + setSearchTerm(updatedValue); + return; + }; /** * The function describes what to do when an item is selected. * - * @param value The value of the selected option. + * @param _ * @param option The selected option. */ - const onMenuItemSelected = (value: string, option: DefaultOptionType): void => { + const onMenuItemSelected = (_: string, option: DefaultOptionType): void => { const selectedFeatures = features?.filter(feat => `${feat.get(idProperty)}` === option.key); if (!_isNil(selectedFeatures) && Array.isArray(selectedFeatures) && selectedFeatures?.length > 0) { onSelect(selectedFeatures[0]); } }; + /** + * Resets input field value and previously fetched data on reset button click. + */ + const resetSearch = () => setSearchTerm(undefined); + + useEffect(() => { + onChange(features); + }, [onChange, features]); + const finalClassName = className ? `${className} ${defaultClassName}` : defaultClassName; + if (asInput) { + return ( + : + + } + value={searchTerm} + /> + ); + } + return ( allowClear={true} @@ -183,6 +235,7 @@ export const WfsSearch: FC = ({ onChange={onUpdateInput} onSelect={onMenuItemSelected} suffixIcon={null} + value={searchTerm} {...passThroughProps} > { @@ -192,4 +245,4 @@ export const WfsSearch: FC = ({ ); }; -export default WfsSearch; +export default WfsSearchField; diff --git a/src/Field/WfsSearchInput/WfsSearchInput.example.md b/src/Field/WfsSearchInput/WfsSearchInput.example.md deleted file mode 100644 index 291ae8a54b..0000000000 --- a/src/Field/WfsSearchInput/WfsSearchInput.example.md +++ /dev/null @@ -1,121 +0,0 @@ -This demonstrates the usage of the WfsSearchInput with presentation of found -results in AgFeatureGrid component. - - -```jsx -import WfsSearchInput from '@terrestris/react-geo/dist/Field/WfsSearchInput/WfsSearchInput'; -import AgFeatureGrid from '@terrestris/react-geo/dist/Grid/AgFeatureGrid/AgFeatureGrid'; -import OlFormatGeoJSON from 'ol/format/GeoJSON'; -import OlLayerTile from 'ol/layer/Tile'; -import OlMap from 'ol/Map'; -import { fromLonLat } from 'ol/proj'; -import OlSourceOSM from 'ol/source/OSM'; -import OlView from 'ol/View'; -import * as React from 'react'; - -class WfsSearchInputExample extends React.Component { - - constructor(props) { - - super(props); - - this.state = { - data: [] - }; - - this.mapDivId = `map-${Math.random()}`; - - this.map = new OlMap({ - layers: [ - new OlLayerTile({ - name: 'OSM', - source: new OlSourceOSM() - }) - ], - view: new OlView({ - center: fromLonLat([37.40570, 8.81566]), - zoom: 4 - }) - }); - } - - componentDidMount() { - this.map.setTarget(this.mapDivId); - } - - onFetchSuccess(data) { - const format = new OlFormatGeoJSON(); - const features = data.map(d => format.readFeature(d)); - this.setState({ - data: features - }); - } - - onClearClick() { - this.setState({ - data: [] - }); - } - - render() { - return ( - - - WFS Search Input: - - { - this.state.data.length > 0 && - - } - - - - - ); - } -} - - -``` diff --git a/src/Field/WfsSearchInput/WfsSearchInput.less b/src/Field/WfsSearchInput/WfsSearchInput.less deleted file mode 100644 index 823d9eb337..0000000000 --- a/src/Field/WfsSearchInput/WfsSearchInput.less +++ /dev/null @@ -1,3 +0,0 @@ -.react-geo-wfssearchinput { - width: 100%; -} diff --git a/src/Field/WfsSearchInput/WfsSearchInput.spec.tsx b/src/Field/WfsSearchInput/WfsSearchInput.spec.tsx deleted file mode 100644 index b385c0ddf0..0000000000 --- a/src/Field/WfsSearchInput/WfsSearchInput.spec.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import Logger from '@terrestris/base-util/dist/Logger'; -import { - act -} from 'react-dom/test-utils'; - -import TestUtil from '../../Util/TestUtil'; -import WfsSearchInput, {WfsSearchInputProps, WfsSearchState} from './WfsSearchInput'; - -describe('', () => { - it('is defined', () => { - expect(WfsSearchInput).not.toBeUndefined(); - }); - - it('can be rendered', () => { - const wrapper = TestUtil.mountComponent(WfsSearchInput); - expect(wrapper).not.toBeUndefined(); - }); - - describe('#onUpdateInput', () => { - it('resets state.data', () => { - const wrapper = TestUtil.mountComponent(WfsSearchInput); - const instance = wrapper.instance() as WfsSearchInput; - instance.onUpdateInput(); - expect((wrapper.state() as WfsSearchState).data).toEqual([]); - }); - - it('sets the inputValue as state.searchTerm', () => { - const wrapper = TestUtil.mountComponent(WfsSearchInput); - const evt = { - target: { - value: 'a' - } - }; - act(() => { - const instance = wrapper.instance() as WfsSearchInput; - instance.onUpdateInput(evt); - }); - expect((wrapper.state() as WfsSearchState).searchTerm).toBe(evt.target.value); - }); - - it('calls onBeforeSearch callback if passed in props', () => { - const wrapper = TestUtil.mountComponent(WfsSearchInput, { - }); - wrapper.setProps({ - onBeforeSearch: jest.fn(), - }); - const evt = { - target: { - value: 'abc' - } - }; - - act(() => { - const instance = wrapper.instance() as WfsSearchInput; - instance.onUpdateInput(evt); - }); - expect((wrapper.props() as WfsSearchInputProps).onBeforeSearch).toHaveBeenCalled(); - }); - - it('sends a request if input is as long as props.minChars', () => { - // expect.assertions(1); - const wrapper = TestUtil.mountComponent(WfsSearchInput, { - placeholder: 'Type a countryname in its own language…', - baseUrl: 'https://ows-demo.terrestris.de/geoserver/osm/wfs', - featureTypes: ['osm:osm-country-borders'], - attributeDetails: { - 'osm:osm-country-borders': { - name: { - type: 'string', - exactSearch: false, - matchCase: false - } - } - } - }); - - const instance = wrapper.instance() as WfsSearchInput; - const doSearchSpy = jest.spyOn(instance, 'doSearch'); - const evt = { - target: { - value: 'abc' - } - }; - act(() => { - instance.onUpdateInput(evt); - }); - expect(doSearchSpy).toHaveBeenCalled(); - doSearchSpy.mockRestore(); - }); - }); - - describe('#onFetchSuccess', () => { - it('sets the response features as state.data', () => { - const wrapper = TestUtil.mountComponent(WfsSearchInput); - const response = { - features: [{ - id: '752526', - properties: { - name: 'Deutschland' - } - }] - }; - const instance = wrapper.instance() as WfsSearchInput; - instance.onFetchSuccess(response); - const promise = new Promise(resolve => { - setTimeout(resolve, 350); - }); - return promise.then(() => { - expect((wrapper.state() as WfsSearchState).data).toEqual(response.features); - }); - }); - }); - - describe('#onFetchError', () => { - it('sets the response as state.data', () => { - const wrapper = TestUtil.mountComponent(WfsSearchInput); - const loggerSpy = jest.spyOn(Logger, 'error'); - const instance = wrapper.instance() as WfsSearchInput; - instance.onFetchError('Peter'); - expect(loggerSpy).toHaveBeenCalled(); - expect(loggerSpy).toHaveBeenCalledWith('Error while requesting WFS GetFeature: Peter'); - loggerSpy.mockRestore(); - }); - }); - - describe('#resetSearch', () => { - it('resets input value', () => { - const wrapper = TestUtil.mountComponent(WfsSearchInput); - const instance = wrapper.instance() as WfsSearchInput; - instance._inputRef.input.value = 'some value'; - instance.resetSearch(); - expect(instance._inputRef.input.value).toBe(''); - }); - - it('resets state value for data', () => { - const wrapper = TestUtil.mountComponent(WfsSearchInput); - act(() => { - wrapper.setState({ - data: [{ - feat1: { - prop: 'peter' - } - }] - }); - }); - expect((wrapper.state() as WfsSearchState).data.length).toBe(1); - act(() => { - const instance = wrapper.instance() as WfsSearchInput; - instance.resetSearch(); - }); - expect((wrapper.state() as WfsSearchState).data.length).toBe(0); - expect((wrapper.state() as WfsSearchState).data).toEqual([]); - - }); - - it('calls onClearClick callback function if passed in props', () => { - const wrapper = TestUtil.mountComponent(WfsSearchInput); - wrapper.setProps({ - onClearClick: jest.fn() - }); - act(() => { - const instance = wrapper.instance() as WfsSearchInput; - instance.resetSearch(); - }); - expect((wrapper.props() as WfsSearchInputProps).onClearClick).toHaveBeenCalled(); - }); - }); -}); diff --git a/src/Field/WfsSearchInput/WfsSearchInput.tsx b/src/Field/WfsSearchInput/WfsSearchInput.tsx deleted file mode 100644 index ee84dd6e17..0000000000 --- a/src/Field/WfsSearchInput/WfsSearchInput.tsx +++ /dev/null @@ -1,463 +0,0 @@ -import './WfsSearchInput.less'; - -import { - faCircleNotch, - faClose -} from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import Logger from '@terrestris/base-util/dist/Logger'; -import WfsFilterUtil, { - AttributeDetails -} from '@terrestris/ol-util/dist/WfsFilterUtil/WfsFilterUtil'; -import { - Input -} from 'antd'; -import { InputProps } from 'antd/lib/input'; -import { Feature } from 'geojson'; -import _debounce from 'lodash/debounce'; -import OlFormatGeoJson from 'ol/format/GeoJSON'; -import OlFormatGML32 from 'ol/format/GML32'; -import OlMap from 'ol/Map'; -import * as React from 'react'; - -import { CSS_PREFIX } from '../../constants'; - -interface OwnProps { - /** - * A nested object mapping feature types to an object of attribute details, - * which are also mapped by search attribute name. - * - * Example: - * ``` - * attributeDetails: { - * featType1: { - * attr1: { - * matchCase: true, - * type: 'number', - * exactSearch: false - * }, - * attr2: { - * matchCase: false, - * type: 'string', - * exactSearch: true - * } - * }, - * featType2: {...} - * } - * ``` - */ - attributeDetails: AttributeDetails; - /** - * SRS name. No srsName attribute will be set on geometries when this is not - * provided. - */ - srsName: string; - /** - * The output format of the response. - */ - outputFormat: string; - /** - * Options which are added to the fetch-POST-request. credentials is set to - * 'same-origin' as default but can be overwritten. See also - * https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch - */ - additionalFetchOptions: any; - /** - * The minimal amount of characters entered in the input to start a search. - */ - minChars: number; - /** - * Delay in ms before actually sending requests. - */ - delay: number; - /** - * Indicate if we should render the input and results. When setting to false, - * you need to handle user input and results yourself - */ - visible?: boolean; - /** - * The searchTerm may be given as prop. This is useful when setting - * `visible` to `false`. - */ - searchTerm?: string; - /** - * An optional CSS class which should be added. - */ - className?: string; - /** - * The base URL. Please make sure that the WFS-Server supports CORS. - */ - baseUrl: string; - - /** - * The namespace URI used for features. - */ - featureNS?: string; - /** - * The prefix for the feature namespace. - */ - featurePrefix?: string; - /** - * The feature type names. - */ - featureTypes: string[]; - /** - * Maximum number of features to fetch. Default value is 1000. - */ - maxFeatures?: number; - /** - * Geometry name to use in a BBOX filter. - */ - geometryName?: string; - /** - * Optional list of property names to serialize. - */ - propertyNames?: string[]; - /** - * The ol.map to interact with on selection. - */ - map: OlMap; - /** - * Optional callback function, that will be called before WFS search starts. - * Can be useful if input value manipulation is needed (e.g. umlaut - * replacement `ä => oa` etc.) - */ - onBeforeSearch?: (value: string) => string; - /** - * An onFetchSuccess callback function which gets called with the - * successfully fetched data. - * Please note: if omitted only data fetch will be performed and no data - * will be shown afterwards! - */ - onFetchSuccess?: (features: Feature[]) => void; - /** - * An onFetchError callback function which gets called if data fetch is - * failed. - */ - onFetchError?: (error: any) => void; - /** - * Optional callback function, that will be called if 'clear' button of - * input field was clicked. - */ - onClearClick?: () => void; - /** - * Options which are passed to the constructor of the ol.format.WFS. - * compare: https://openlayers.org/en/latest/apidoc/module-ol_format_WFS.html - */ - wfsFormatOptions?: any; -} - -export interface WfsSearchState { - searchTerm: string; - data: Feature[]; - fetching: boolean; -} - -export type WfsSearchInputProps = OwnProps & InputProps; - -/** - * The WfsSearchInput field. - * Implements an input field to do a WFS-GetFeature request. - * - * The GetFeature request is created with `ol.format.WFS.writeGetFeature` - * so most of the WFS specific options work like document in the corresponding - * API-docs: https://openlayers.org/en/latest/apidoc/module-ol_format_WFS.html - * - * @class WfsSearchInput - * @extends React.Component - */ -export class WfsSearchInput extends React.Component { - - static defaultProps = { - srsName: 'EPSG:3857', - outputFormat: 'application/json', - minChars: 3, - additionalFetchOptions: {}, - attributeDetails: {}, - delay: 300, - visible: true - }; - - /** - * The reference to the Input Element of the WfsSearch. - * @private - */ - _inputRef: any; - - /** - * The className added to this component. - * @private - */ - className = `${CSS_PREFIX}wfssearchinput`; - - /** - * Create the WfsSearchInput. - * - * @param props The initial props. - * @constructs WfsSearchInput - */ - constructor(props: WfsSearchInputProps) { - super(props); - this.state = { - searchTerm: '', - data: [], - fetching: false - }; - this.onUpdateInput = this.onUpdateInput.bind(this); - // delay requests invoking - this.doSearch = _debounce(this.doSearch, this.props.delay); - } - - /** - * Trigger search when searchTerm prop has changed - * - * @param prevProps Previous props - */ - componentDidUpdate(prevProps: Readonly) { - if (this.props.searchTerm !== prevProps.searchTerm) { - const evt = { - target: { - value: this.props.searchTerm - } - }; - this.onUpdateInput(evt); - } - } - - /** - * Called if the input is being updated. It sets the current inputValue - * as searchTerm and starts a search if the inputValue has - * a length of at least `this.props.minChars` (default 3). - * - * @param evt The input value from `keyup` event. - * Gets undefined if clear btn is pressed. - */ - onUpdateInput(evt?: any) { - const { - minChars, - onBeforeSearch - } = this.props; - - this.setState({ - data: [] - }); - - let value = ''; - - if (evt && evt.target && evt.target.value) { - value = evt.target.value; - } - - if (onBeforeSearch) { - value = onBeforeSearch(value); - } - - this.setState({ - searchTerm: value - }, () => { - if (this.state.searchTerm && this.state.searchTerm.length >= minChars) { - this.doSearch(); - } - }); - - } - - /** - * Perform the search. - * @private - */ - doSearch() { - const { - additionalFetchOptions, - baseUrl, - featureNS, - featurePrefix, - featureTypes, - geometryName, - maxFeatures = 1000, - outputFormat, - propertyNames, - srsName, - wfsFormatOptions, - attributeDetails - } = this.props; - - const searchOpts = { - featureNS: featureNS ?? '', - featurePrefix: featurePrefix ?? '', - featureTypes, - geometryName: geometryName ?? '', - maxFeatures, - outputFormat, - propertyNames: propertyNames ?? [], - srsName, - wfsFormatOptions, - attributeDetails - }; - - const request = WfsFilterUtil.getCombinedRequests( - searchOpts, this.state.searchTerm - ); - - if (request) { - this.setState({ - fetching: true - }, async () => { - try { - const response = await fetch(`${baseUrl}`, { - method: 'POST', - credentials: additionalFetchOptions.credentials - ? additionalFetchOptions.credentials - : 'same-origin', - body: new XMLSerializer().serializeToString(request), - ...additionalFetchOptions - }); - let data; - if (outputFormat === 'application/json' ) { - data = await response.json(); - } else { - const xml = await response.text(); - // TODO: Add support for other GML formats - const gmlParser = new OlFormatGML32(); - const geojsonParser = new OlFormatGeoJson(); - const features = gmlParser.readFeatures(xml); - data = geojsonParser.writeFeaturesObject(features); - } - this.onFetchSuccess(data); - } catch (e) { - this.onFetchError(e); - } - }); - } else { - this.onFetchError('Missing GetFeature request parameters'); - } - } - - /** - * This function gets called on success of the WFS GetFeature fetch request. - * It sets the response as data. - * If an additional function `onFetchSuccess` is provided, it will be called - * as callback. - * @param response The found features. - */ - onFetchSuccess(response: any) { - const { - onFetchSuccess - } = this.props; - const data: Feature[] = response.features ? response.features : []; - for (const feature of data) { - if (!feature.properties) { - feature.properties = {}; - } - feature.properties.searchTerm = this.state.searchTerm; - } - this.setState({ - data, - fetching: false - }, () => { - onFetchSuccess?.(data); - }); - } - - /** - * This function gets called when the WFS GetFeature fetch request returns an error. - * It logs the error to the console. - * If an additional function `onFetchSuccess` is provided, it will be called - * as callback. - * - * @param error The errorstring. - */ - onFetchError(error: any) { - const { - onFetchError - } = this.props; - Logger.error(`Error while requesting WFS GetFeature: ${error}`); - this.setState({ - fetching: false - }, () => { - onFetchError?.(error); - }); - } - - /** - * Resets input field value and previously fetched data on reset button click. - */ - resetSearch() { - const { - onClearClick - } = this.props; - this._inputRef.input.value = ''; - this.setState({ - data: [] - }, () => { - if (onClearClick) { - onClearClick(); - } - }); - } - - /** - * The render function. - */ - render() { - const { - fetching - } = this.state; - - const { - additionalFetchOptions, - baseUrl, - className, - featureNS, - featurePrefix, - featureTypes, - geometryName, - map, - maxFeatures, - minChars, - outputFormat, - propertyNames, - attributeDetails, - srsName, - wfsFormatOptions, - visible, - onFetchSuccess, - onFetchError, - onClearClick, - onBeforeSearch, - ...passThroughProps - } = this.props; - - if (visible === false) { - return null; - } - - const finalClassName = className - ? `${className} ${this.className}` - : this.className; - - return ( - {this._inputRef = inputRef; }} - suffix={ - fetching ? - : - - } - onPressEnter={this.onUpdateInput} - onKeyUp={this.onUpdateInput} - {...passThroughProps} - /> - ); - } -} - -export default WfsSearchInput; diff --git a/src/index.ts b/src/index.ts index fe6a5f5a85..3941212fd9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,8 +20,7 @@ import FeatureLabelModal from './FeatureLabelModal/FeatureLabelModal'; import CoordinateReferenceSystemCombo from './Field/CoordinateReferenceSystemCombo/CoordinateReferenceSystemCombo'; import NominatimSearch from './Field/NominatimSearch/NominatimSearch'; import ScaleCombo from './Field/ScaleCombo/ScaleCombo'; -import WfsSearch from './Field/WfsSearch/WfsSearch'; -import WfsSearchInput from './Field/WfsSearchInput/WfsSearchInput'; +import WfsSearchField from './Field/WfsSearchField/WfsSearchField'; import AgFeatureGrid from './Grid/AgFeatureGrid/AgFeatureGrid'; import FeatureGrid from './Grid/FeatureGrid/FeatureGrid'; import PropertyGrid from './Grid/PropertyGrid/PropertyGrid'; @@ -68,8 +67,7 @@ export { ToggleButton, ToggleGroup, UploadButton, - WfsSearch, - WfsSearchInput, + WfsSearchField, ZoomButton, ZoomToExtentButton };