From 28a1f7a78a46255dbaea9422d468fc7e9b27a899 Mon Sep 17 00:00:00 2001 From: Daniel Lautzenheiser Date: Wed, 9 Nov 2022 09:52:05 -0500 Subject: [PATCH] Fix/bugfixes (#80) * Fix widgetsFor for lists and objects * Fix input for datetime widget * Fix search --- core/dev-test/config.yml | 2 +- core/dev-test/index.js | 2 +- core/src/actions/search.ts | 2 +- core/src/backend.ts | 5 +- core/src/components/Collection/Collection.tsx | 22 ++- .../Collection/CollectionControls.tsx | 52 ++--- .../Collection/CollectionSearch.tsx | 183 +++++++++++------- .../components/Collection/Entries/Entries.tsx | 2 +- .../Collection/Entries/EntriesSearch.tsx | 41 ++-- .../Collection/Entries/EntryListing.tsx | 11 +- .../EditorPreviewContent.tsx | 20 +- .../EditorPreviewPane/EditorPreviewPane.tsx | 85 ++++++-- core/src/lib/util/Cursor.ts | 2 +- core/src/widgets/datetime/DateTimeControl.tsx | 19 +- 14 files changed, 270 insertions(+), 178 deletions(-) diff --git a/core/dev-test/config.yml b/core/dev-test/config.yml index 483b0912..e2e23c48 100644 --- a/core/dev-test/config.yml +++ b/core/dev-test/config.yml @@ -628,7 +628,7 @@ collections: widget: select options: - label: One - value: "One" + value: 'One' - label: Two value: 2 - label: Three diff --git a/core/dev-test/index.js b/core/dev-test/index.js index 948e4454..37e24961 100644 --- a/core/dev-test/index.js +++ b/core/dev-test/index.js @@ -1,7 +1,7 @@ // Register all the things CMS.init(); -const PostPreview = ({ entry, widgetFor }) => { +const PostPreview = ({ entry, widgetFor, widgetsFor }) => { return h( 'div', {}, diff --git a/core/src/actions/search.ts b/core/src/actions/search.ts index dd725f5d..eba5e056 100644 --- a/core/src/actions/search.ts +++ b/core/src/actions/search.ts @@ -140,7 +140,7 @@ export function searchEntries(searchTerm: string, searchCollections: string[], p return dispatch(searchFailure(new Error(`No integration found for name "${integration}"`))); } - return dispatch(searchSuccess(response.entries, response.pagination)); + return dispatch(searchSuccess(response.entries, page)); } catch (error: unknown) { console.error(error); if (error instanceof Error) { diff --git a/core/src/backend.ts b/core/src/backend.ts index 6f49c0f4..237ada0e 100644 --- a/core/src/backend.ts +++ b/core/src/backend.ts @@ -261,7 +261,8 @@ interface PersistArgs { function collectionDepth(collection: Collection) { let depth; - depth = 'nested' in collection && collection.nested?.depth || getPathDepth(collection.path ?? ''); + depth = + ('nested' in collection && collection.nested?.depth) || getPathDepth(collection.path ?? ''); if (hasI18n(collection)) { depth = getI18nFilesDepth(collection, depth); @@ -549,7 +550,7 @@ export class Backend { } const hits = entries - .filter(({ score }: fuzzy.FilterResult) => score > 5) + .filter(({ score }: fuzzy.FilterResult) => score > 3) .sort(sortByScore) .map((f: fuzzy.FilterResult) => f.original); return { entries: hits, pagination: 1 }; diff --git a/core/src/components/Collection/Collection.tsx b/core/src/components/Collection/Collection.tsx index 69648d3b..bfeba861 100644 --- a/core/src/components/Collection/Collection.tsx +++ b/core/src/components/Collection/Collection.tsx @@ -110,7 +110,14 @@ const CollectionView = ({ } } - return ; + return ( + + ); } return ( @@ -210,11 +217,14 @@ const CollectionView = ({ <> {isSearchResults ? ( - - - {t(searchResultKey, { searchTerm, collection: collection.label })} - - + <> + + + {t(searchResultKey, { searchTerm, collection: collection.label })} + + + + ) : ( <> diff --git a/core/src/components/Collection/CollectionControls.tsx b/core/src/components/Collection/CollectionControls.tsx index 76e5580a..b743bf4b 100644 --- a/core/src/components/Collection/CollectionControls.tsx +++ b/core/src/components/Collection/CollectionControls.tsx @@ -33,15 +33,15 @@ const CollectionControlsContainer = styled('div')` interface CollectionControlsProps { viewStyle: CollectionViewStyle; onChangeViewStyle: (viewStyle: CollectionViewStyle) => void; - sortableFields: SortableField[]; - onSortClick: (key: string, direction?: SortDirection) => Promise; - sort: SortMap | undefined; - filter: Record; - viewFilters: ViewFilter[]; - onFilterClick: (filter: ViewFilter) => void; - group: Record; - viewGroups: ViewGroup[]; - onGroupClick: (filter: ViewGroup) => void; + sortableFields?: SortableField[]; + onSortClick?: (key: string, direction?: SortDirection) => Promise; + sort?: SortMap | undefined; + filter?: Record; + viewFilters?: ViewFilter[]; + onFilterClick?: (filter: ViewFilter) => void; + group?: Record; + viewGroups?: ViewGroup[]; + onGroupClick?: (filter: ViewGroup) => void; } const CollectionControls = ({ @@ -61,20 +61,26 @@ const CollectionControls = ({ return ( - {viewGroups.length > 0 && ( - - )} - {viewFilters.length > 0 && ( - - )} - {sortableFields.length > 0 && ( - - )} + {viewGroups && onGroupClick && group + ? viewGroups.length > 0 && ( + + ) + : null} + {viewFilters && onFilterClick && filter + ? viewFilters.length > 0 && ( + + ) + : null} + {sortableFields && onSortClick && sort + ? sortableFields.length > 0 && ( + + ) + : null} ); }; diff --git a/core/src/components/Collection/CollectionSearch.tsx b/core/src/components/Collection/CollectionSearch.tsx index 148b25c0..c754b6ef 100644 --- a/core/src/components/Collection/CollectionSearch.tsx +++ b/core/src/components/Collection/CollectionSearch.tsx @@ -1,37 +1,27 @@ -import { styled } from '@mui/material/styles'; import SearchIcon from '@mui/icons-material/Search'; import InputAdornment from '@mui/material/InputAdornment'; +import Popover from '@mui/material/Popover'; +import { styled } from '@mui/material/styles'; import TextField from '@mui/material/TextField'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { translate } from 'react-polyglot'; +import { colors, colorsRaw, lengths } from '../../components/UI/styles'; import { transientOptions } from '../../lib'; -import { colors, colorsRaw, lengths, zIndex } from '../../components/UI/styles'; -import type { KeyboardEvent, MouseEvent } from 'react'; +import type { ChangeEvent, FocusEvent, KeyboardEvent, MouseEvent } from 'react'; import type { Collection, Collections, TranslatedProps } from '../../interface'; const SearchContainer = styled('div')` position: relative; `; -const SuggestionsContainer = styled('div')` - position: relative; - width: 100%; -`; - const Suggestions = styled('ul')` - position: absolute; - top: 0px; - left: 0; - right: 0; padding: 10px 0; margin: 0; list-style: none; - background-color: #fff; border-radius: ${lengths.borderRadius}; - border: 1px solid ${colors.textFieldBorder}; - z-index: ${zIndex.zIndex1}; + width: 240px; `; const SuggestionHeader = styled('li')` @@ -66,6 +56,10 @@ const SuggestionDivider = styled('div')` width: 100%; `; +const StyledPopover = styled(Popover)` + margin-left: -44px; +`; + interface CollectionSearchProps { collections: Collections; collection?: Collection; @@ -74,17 +68,34 @@ interface CollectionSearchProps { } const CollectionSearch = ({ - collections, + collections: collectionsMap, collection, searchTerm, onSubmit, t, }: TranslatedProps) => { + const inputRef = useRef(); const [query, setQuery] = useState(searchTerm); - const [suggestionsVisible, setSuggestionsVisible] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const collections = useMemo(() => Object.values(collectionsMap), [collectionsMap]); + + const handleClose = useCallback(() => { + setAnchorEl(null); + inputRef.current?.blur(); + }, []); + + const handleFocus = useCallback((event: FocusEvent) => { + setAnchorEl(event.currentTarget); + }, []); + + const handleBlur = useCallback(() => { + setAnchorEl(null); + }, []); const getSelectedSelectionBasedOnProps = useCallback(() => { - return collection ? Object.keys(collections).indexOf(collection.name) : -1; + return collection ? collections.findIndex(c => c.name === collection.name) : -1; }, [collection, collections]); const [selectedCollectionIdx, setSelectedCollectionIdx] = useState( @@ -92,50 +103,62 @@ const CollectionSearch = ({ ); const [prevCollection, setPrevCollection] = useState(collection); + console.log('selectedCollectionIdx', selectedCollectionIdx); + useEffect(() => { if (prevCollection !== collection) { + console.log( + 'resetting to ', + getSelectedSelectionBasedOnProps(), + 'collection', + collection, + 'prev', + prevCollection, + ); setSelectedCollectionIdx(getSelectedSelectionBasedOnProps()); } setPrevCollection(collection); }, [collection, getSelectedSelectionBasedOnProps, prevCollection]); - const toggleSuggestions = useCallback((visible: boolean) => { - setSuggestionsVisible(visible); - }, []); - const selectNextSuggestion = useCallback(() => { - setSelectedCollectionIdx( - Math.min(selectedCollectionIdx + 1, Object.keys(collections).length - 1), - ); + console.log('selectNextSuggestion'); + setSelectedCollectionIdx(Math.min(selectedCollectionIdx + 1, collections.length - 1)); }, [collections, selectedCollectionIdx]); const selectPreviousSuggestion = useCallback(() => { + console.log('selectPreviousSuggestion'); setSelectedCollectionIdx(Math.max(selectedCollectionIdx - 1, -1)); }, [selectedCollectionIdx]); const resetSelectedSuggestion = useCallback(() => { + console.log('resetSelectedSuggestion'); setSelectedCollectionIdx(-1); }, []); - const submitSearch = useCallback(() => { - toggleSuggestions(false); - if (selectedCollectionIdx !== -1) { - onSubmit(query, Object.values(collections)[selectedCollectionIdx]?.name); - } else { - onSubmit(query); - } - }, [collections, onSubmit, query, selectedCollectionIdx, toggleSuggestions]); + const submitSearch = useCallback( + (index: number) => { + console.log('searching selectedCollectionIdx', index); + if (index !== -1) { + console.log(collections, index); + onSubmit(query, collections[index]?.name); + } else { + onSubmit(query); + } + handleClose(); + }, + [collections, handleClose, onSubmit, query], + ); const handleKeyDown = useCallback( (event: KeyboardEvent) => { if (event.key === 'Enter') { - submitSearch(); + submitSearch(selectedCollectionIdx); } - if (suggestionsVisible) { + if (open) { // allow closing of suggestions with escape key if (event.key === 'Escape') { - toggleSuggestions(false); + handleClose(); } if (event.key === 'ArrowDown') { @@ -148,30 +171,37 @@ const CollectionSearch = ({ } }, [ + handleClose, + open, selectNextSuggestion, selectPreviousSuggestion, + selectedCollectionIdx, submitSearch, - suggestionsVisible, - toggleSuggestions, ], ); const handleQueryChange = useCallback( - (newQuery: string) => { + (event: ChangeEvent) => { + const newQuery = event.target.value; setQuery(newQuery); - toggleSuggestions(newQuery !== ''); - if (newQuery === '') { - resetSelectedSuggestion(); + + if (newQuery !== '') { + setAnchorEl(event.currentTarget); + return; } + + resetSelectedSuggestion(); + handleClose(); }, - [resetSelectedSuggestion, toggleSuggestions], + [handleClose, resetSelectedSuggestion], ); const handleSuggestionClick = useCallback( (event: MouseEvent, idx: number) => { - setSelectedCollectionIdx(idx); - submitSearch(); event.preventDefault(); + console.log('clicked index', idx); + setSelectedCollectionIdx(idx); + submitSearch(idx); }, [submitSearch], ); @@ -180,16 +210,16 @@ const CollectionSearch = ({ toggleSuggestions(true)} placeholder={t('collection.sidebar.searchAll')} - onBlur={() => toggleSuggestions(false)} - onFocus={() => toggleSuggestions(query !== '')} + onBlur={handleBlur} + onFocus={handleFocus} value={query} - onChange={e => handleQueryChange(e.target.value)} + onChange={handleQueryChange} variant="outlined" size="small" fullWidth InputProps={{ + inputRef, startAdornment: ( @@ -197,31 +227,42 @@ const CollectionSearch = ({ ), }} /> - {suggestionsVisible && ( - - - {t('collection.sidebar.searchIn')} + + + {t('collection.sidebar.searchIn')} + handleSuggestionClick(e, -1)} + onMouseDown={e => e.preventDefault()} + > + {t('collection.sidebar.allCollections')} + + + {collections.map((collection, idx) => ( handleSuggestionClick(e, -1)} + key={idx} + $isActive={idx === selectedCollectionIdx} + onClick={e => handleSuggestionClick(e, idx)} onMouseDown={e => e.preventDefault()} > - {t('collection.sidebar.allCollections')} + {collection.label} - - {Object.values(collections).map((collection, idx) => ( - handleSuggestionClick(e, idx)} - onMouseDown={e => e.preventDefault()} - > - {collection.label} - - ))} - - - )} + ))} + + ); }; diff --git a/core/src/components/Collection/Entries/Entries.tsx b/core/src/components/Collection/Entries/Entries.tsx index de66720d..08be5d70 100644 --- a/core/src/components/Collection/Entries/Entries.tsx +++ b/core/src/components/Collection/Entries/Entries.tsx @@ -24,7 +24,7 @@ export interface BaseEntriesProps { isFetching: boolean; viewStyle: CollectionViewStyle; cursor: Cursor; - handleCursorActions: (action: string) => void; + handleCursorActions?: (action: string) => void; } export interface SingleCollectionEntriesProps extends BaseEntriesProps { diff --git a/core/src/components/Collection/Entries/EntriesSearch.tsx b/core/src/components/Collection/Entries/EntriesSearch.tsx index e1e99e02..92d2b2ed 100644 --- a/core/src/components/Collection/Entries/EntriesSearch.tsx +++ b/core/src/components/Collection/Entries/EntriesSearch.tsx @@ -1,5 +1,5 @@ import isEqual from 'lodash/isEqual'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { connect } from 'react-redux'; import { @@ -11,6 +11,7 @@ import { selectSearchedEntries } from '../../../reducers'; import Entries from './Entries'; import type { ConnectedProps } from 'react-redux'; +import type { CollectionViewStyle } from '../../../constants/collectionViews'; import type { Collections } from '../../../interface'; import type { RootState } from '../../../store'; @@ -20,35 +21,24 @@ const EntriesSearch = ({ isFetching, page, searchTerm, + viewStyle, searchEntries, - collectionNames, clearSearch, }: EntriesSearchProps) => { + const collectionNames = useMemo(() => Object.keys(collections), [collections]); + const getCursor = useCallback(() => { return Cursor.create({ actions: Number.isNaN(page) ? [] : ['append_next'], }); }, [page]); - const handleCursorActions = useCallback( - (action: string) => { - if (action === 'append_next') { - const nextPage = page + 1; - searchEntries(searchTerm, collectionNames, nextPage); - } - }, - [collectionNames, page, searchEntries, searchTerm], - ); - - useEffect(() => { - searchEntries(searchTerm, collectionNames); - }, [collectionNames, searchEntries, searchTerm]); - useEffect(() => { return () => { clearSearch(); }; - }, [clearSearch]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const [prevSearch, setPrevSearch] = useState(''); const [prevCollectionNames, setPrevCollectionNames] = useState([]); @@ -61,16 +51,19 @@ const EntriesSearch = ({ setPrevSearch(searchTerm); setPrevCollectionNames(collectionNames); - searchEntries(searchTerm, collectionNames); - }, [collectionNames, prevCollectionNames, prevSearch, searchEntries, searchTerm]); + setTimeout(() => { + searchEntries(searchTerm, collectionNames); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [collectionNames, prevCollectionNames, prevSearch, searchTerm]); return ( ); }; @@ -78,16 +71,16 @@ const EntriesSearch = ({ interface EntriesSearchOwnProps { searchTerm: string; collections: Collections; + viewStyle: CollectionViewStyle; } function mapStateToProps(state: RootState, ownProps: EntriesSearchOwnProps) { - const { searchTerm } = ownProps; - const collections = Object.values(ownProps.collections); - const collectionNames = Object.keys(ownProps.collections); + const { searchTerm, collections, viewStyle } = ownProps; + const collectionNames = Object.keys(collections); const isFetching = state.search.isFetching; const page = state.search.page; const entries = selectSearchedEntries(state, collectionNames); - return { isFetching, page, collections, collectionNames, entries, searchTerm }; + return { isFetching, page, collections, viewStyle, entries, searchTerm }; } const mapDispatchToProps = { diff --git a/core/src/components/Collection/Entries/EntryListing.tsx b/core/src/components/Collection/Entries/EntryListing.tsx index c6c057c2..72e4990b 100644 --- a/core/src/components/Collection/Entries/EntryListing.tsx +++ b/core/src/components/Collection/Entries/EntryListing.tsx @@ -41,7 +41,7 @@ export interface BaseEntryListingProps { entries: Entry[]; viewStyle: CollectionViewStyle; cursor?: Cursor; - handleCursorActions: (action: string) => void; + handleCursorActions?: (action: string) => void; page?: number; } @@ -65,14 +65,11 @@ const EntryListing = ({ handleCursorActions, ...otherProps }: EntryListingProps) => { - const hasMore = useMemo(() => { - const hasMore = cursor?.actions?.has('append_next'); - return hasMore; - }, [cursor?.actions]); + const hasMore = useMemo(() => cursor?.actions?.has('append_next'), [cursor?.actions]); const handleLoadMore = useCallback(() => { if (hasMore) { - handleCursorActions('append_next'); + handleCursorActions?.('append_next'); } }, [handleCursorActions, hasMore]); @@ -138,7 +135,7 @@ const EntryListing = ({
{renderedCards} - {hasMore && } + {hasMore && handleLoadMore && }
); diff --git a/core/src/components/Editor/EditorPreviewPane/EditorPreviewContent.tsx b/core/src/components/Editor/EditorPreviewPane/EditorPreviewContent.tsx index 76379495..f89142f5 100644 --- a/core/src/components/Editor/EditorPreviewPane/EditorPreviewContent.tsx +++ b/core/src/components/Editor/EditorPreviewPane/EditorPreviewContent.tsx @@ -1,6 +1,5 @@ -import React, { useMemo } from 'react'; +import React, { memo } from 'react'; -import type { ReactNode } from 'react'; import type { TemplatePreviewComponent, TemplatePreviewProps } from '../../../interface'; interface EditorPreviewContentProps { @@ -8,17 +7,16 @@ interface EditorPreviewContentProps { previewProps: TemplatePreviewProps; } -const EditorPreviewContent = ({ previewComponent, previewProps }: EditorPreviewContentProps) => { - return useMemo(() => { - let children: ReactNode; +const EditorPreviewContent = memo( + ({ previewComponent, previewProps }: EditorPreviewContentProps) => { if (!previewComponent) { - children = null; - } else { - children = React.createElement(previewComponent, previewProps); + return null; } - return children; - }, [previewComponent, previewProps]); -}; + return React.createElement(previewComponent, previewProps); + }, +); + +EditorPreviewContent.displayName = 'EditorPreviewContent'; export default EditorPreviewContent; diff --git a/core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.tsx b/core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.tsx index 88219c86..14e6323f 100644 --- a/core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.tsx +++ b/core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.tsx @@ -29,6 +29,7 @@ import type { Field, GetAssetFunction, ListField, + ObjectValue, RenderedField, TemplatePreviewProps, TranslatedProps, @@ -367,36 +368,92 @@ const PreviewPane = (props: TranslatedProps) => { */ const widgetsFor = useCallback( (name: string) => { - const field = fields.find(f => f.name === name); - if (!field) { + const cmsConfig = config.config; + if (!cmsConfig) { return { data: null, widgets: {}, }; } - const nestedFields = field && 'fields' in field ? field.fields ?? [] : []; - const value = entry.data?.[field.name] as EntryData | EntryData[]; + const field = fields.find(f => f.name === name); + if (!field || !('fields' in field)) { + return { + data: null, + widgets: {}, + }; + } - if (Array.isArray(value)) { - return value.map(val => { - const widgets = nestedFields.reduce((acc, field, index) => { - acc[field.name] =
{widgetFor(field.name)}
; - return acc; - }, {} as Record); - return { data: val, widgets }; - }); + const value = entry.data?.[field.name]; + const nestedFields = field && 'fields' in field ? field.fields ?? [] : []; + + if (field.widget === 'list' || Array.isArray(value)) { + let finalValue: ObjectValue[]; + if (!value || typeof value !== 'object') { + finalValue = []; + } else if (!Array.isArray(value)) { + finalValue = [value]; + } else { + finalValue = value as ObjectValue[]; + } + + return finalValue + .filter((val: unknown) => typeof val === 'object') + .map((val: ObjectValue) => { + const widgets = nestedFields.reduce((acc, field, index) => { + acc[field.name] = ( +
+ {getWidgetFor( + cmsConfig, + collection, + field.name, + fields, + entry, + inferedFields, + handleGetAsset, + nestedFields, + val, + index, + )} +
+ ); + return acc; + }, {} as Record); + return { data: val, widgets }; + }); + } + + if (typeof value !== 'object') { + return { + data: {}, + widgets: {}, + }; } return { data: value, widgets: nestedFields.reduce((acc, field, index) => { - acc[field.name] =
{widgetFor(field.name)}
; + acc[field.name] = ( +
+ {getWidgetFor( + cmsConfig, + collection, + field.name, + fields, + entry, + inferedFields, + handleGetAsset, + nestedFields, + value, + index, + )} +
+ ); return acc; }, {} as Record), }; }, - [entry.data, fields, widgetFor], + [collection, config.config, entry, fields, handleGetAsset, inferedFields], ); const previewStyles = useMemo( diff --git a/core/src/lib/util/Cursor.ts b/core/src/lib/util/Cursor.ts index 3478d3d8..db1be966 100644 --- a/core/src/lib/util/Cursor.ts +++ b/core/src/lib/util/Cursor.ts @@ -19,7 +19,7 @@ const knownMetaKeys = [ ]; function filterUnknownMetaKeys(meta: Record) { - return Object.keys(meta).reduce((acc, k) => { + return Object.keys(meta ?? {}).reduce((acc, k) => { if (knownMetaKeys.includes(k)) { acc[k] = meta[k]; } diff --git a/core/src/widgets/datetime/DateTimeControl.tsx b/core/src/widgets/datetime/DateTimeControl.tsx index 046614f8..8cb6dba7 100644 --- a/core/src/widgets/datetime/DateTimeControl.tsx +++ b/core/src/widgets/datetime/DateTimeControl.tsx @@ -13,7 +13,6 @@ import parse from 'date-fns/parse'; import parseISO from 'date-fns/parseISO'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import alert from '../../components/UI/Alert'; import { isNotEmpty } from '../../lib/util/string.util'; import type { MouseEvent } from 'react'; @@ -169,16 +168,7 @@ const DateTimeControl = ({ return null; } - let formattedValue = defaultValue; - try { - formattedValue = formatDate(field.picker_utc ? utcDate : dateValue, inputFormat); - } catch (e) { - alert({ - title: 'editor.editorWidgets.datetime.invalidDateTitle', - body: 'editor.editorWidgets.datetime.invalidDateBody', - }); - console.error(e); - } + const inputDate = field.picker_utc ? utcDate : dateValue; if (dateFormat && !timeFormat) { return ( @@ -186,7 +176,7 @@ const DateTimeControl = ({ key="mobile-date-picker" inputFormat={inputFormat} label={label} - value={formattedValue} + value={inputDate} onChange={handleChange} renderInput={params => ( ( (