Skip to content

Commit 4fb353a

Browse files
authored
Merge pull request #794 from amitamrutiya/user-serach-fiel
Convert Autocomplete input and user serach field into sistent
2 parents 742c66f + 74bf9bc commit 4fb353a

File tree

10 files changed

+624
-0
lines changed

10 files changed

+624
-0
lines changed
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { Autocomplete } from '@mui/material';
2+
import React, { useCallback, useEffect, useState } from 'react';
3+
import { Box, Chip, CircularProgress, Grid, TextField, Tooltip, Typography } from '../../base';
4+
import { iconLarge, iconSmall } from '../../constants/iconsSizes';
5+
import { CloseIcon, OrgIcon } from '../../icons';
6+
7+
interface Option {
8+
id: string;
9+
name: string;
10+
}
11+
12+
interface InputSearchFieldProps {
13+
data: Option[];
14+
setFilterData: (data: Option[]) => void;
15+
label?: string;
16+
fetchSuggestions: (value: string) => void;
17+
isLoading: boolean;
18+
type: string;
19+
disabled?: boolean;
20+
selectedData: Option[];
21+
searchValue: string;
22+
setSearchValue: (value: string) => void;
23+
}
24+
25+
const InputSearchField: React.FC<InputSearchFieldProps> = ({
26+
data,
27+
label,
28+
fetchSuggestions,
29+
setFilterData,
30+
isLoading,
31+
type,
32+
disabled,
33+
selectedData,
34+
searchValue,
35+
setSearchValue
36+
}) => {
37+
const [error, setError] = useState('');
38+
const [open, setOpen] = useState(false);
39+
const [showAllItems, setShowAllItems] = useState(false);
40+
const [localSelectedData, setLocalSelectedData] = useState<Option[]>(selectedData);
41+
42+
// Sync local state with prop changes
43+
useEffect(() => {
44+
setLocalSelectedData(selectedData);
45+
}, [selectedData]);
46+
47+
const handleDelete = useCallback(
48+
(id: string) => {
49+
const newData = localSelectedData.filter((item) => item.id !== id);
50+
setLocalSelectedData(newData);
51+
setFilterData(newData);
52+
},
53+
[localSelectedData, setFilterData]
54+
);
55+
56+
const handleAdd = useCallback(
57+
(_event: React.SyntheticEvent, value: Option | null) => {
58+
if (!value) return;
59+
60+
// Check for duplicates
61+
const isDuplicate = localSelectedData.some((item) => item.id === value.id);
62+
if (isDuplicate) {
63+
setError(`${type} already selected`);
64+
return;
65+
}
66+
67+
// Update both local and parent state
68+
const newData = [...localSelectedData, value];
69+
setLocalSelectedData(newData);
70+
setFilterData(newData);
71+
setError('');
72+
setSearchValue('');
73+
setOpen(false);
74+
},
75+
[localSelectedData, setFilterData, type, setSearchValue]
76+
);
77+
78+
const handleInputChange = useCallback(
79+
(_event: React.SyntheticEvent, value: string) => {
80+
setSearchValue(value);
81+
if (value === '') {
82+
setOpen(false);
83+
} else {
84+
const encodedValue = encodeURIComponent(value);
85+
fetchSuggestions(encodedValue);
86+
setError('');
87+
setOpen(true);
88+
}
89+
},
90+
[fetchSuggestions, setSearchValue]
91+
);
92+
93+
return (
94+
<Box sx={{ width: '100%' }}>
95+
<Autocomplete
96+
id={`${type}-search-field`}
97+
style={{ width: '100%' }}
98+
options={data}
99+
getOptionLabel={() => searchValue}
100+
isOptionEqualToValue={(option: Option, value: Option) => option.id === value.id}
101+
noOptionsText={isLoading ? 'Loading...' : `No ${type} found`}
102+
loading={isLoading}
103+
open={open}
104+
onClose={() => setOpen(false)}
105+
disabled={disabled}
106+
value={undefined}
107+
inputValue={searchValue}
108+
onChange={handleAdd}
109+
onInputChange={handleInputChange}
110+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
111+
// @ts-ignore
112+
filterOptions={(x) => x}
113+
disableClearable
114+
includeInputInList
115+
filterSelectedOptions
116+
disableListWrap
117+
clearOnBlur
118+
popupIcon={null}
119+
blurOnSelect
120+
forcePopupIcon={false}
121+
renderInput={(params) => (
122+
<TextField
123+
{...params}
124+
label={label || `Add ${type}`}
125+
error={!!error}
126+
helperText={error}
127+
fullWidth
128+
InputProps={{
129+
...params.InputProps,
130+
endAdornment: (
131+
<React.Fragment>
132+
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
133+
</React.Fragment>
134+
)
135+
}}
136+
/>
137+
)}
138+
renderOption={(props, option: Option) => (
139+
<li {...props} key={option.id}>
140+
<Box component="li" sx={{ '& > img': { mr: 2, flexShrink: 0 } }}>
141+
<Grid container alignItems="center">
142+
<Grid item>
143+
<Box sx={{ color: 'text.secondary', mr: 2 }}>
144+
<OrgIcon {...iconLarge} />
145+
</Box>
146+
</Grid>
147+
<Grid item xs>
148+
<Typography variant="body2">{option.name}</Typography>
149+
</Grid>
150+
</Grid>
151+
</Box>
152+
</li>
153+
)}
154+
/>
155+
156+
<Box
157+
sx={{
158+
display: 'flex',
159+
flexWrap: 'wrap',
160+
gap: 0.5,
161+
mt: localSelectedData?.length > 0 ? '0.5rem' : ''
162+
}}
163+
>
164+
{!showAllItems && localSelectedData?.length > 0 && (
165+
<Chip
166+
key={localSelectedData[localSelectedData.length - 1]?.id}
167+
avatar={<OrgIcon {...iconSmall} />}
168+
label={localSelectedData[localSelectedData.length - 1]?.name}
169+
size="small"
170+
onDelete={() => handleDelete(localSelectedData[localSelectedData.length - 1]?.id)}
171+
deleteIcon={
172+
<Tooltip title={`Remove ${type}`}>
173+
<CloseIcon style={iconSmall} />
174+
</Tooltip>
175+
}
176+
/>
177+
)}
178+
{showAllItems &&
179+
localSelectedData?.map((obj) => (
180+
<Chip
181+
key={obj.id}
182+
avatar={<OrgIcon {...iconSmall} />}
183+
label={obj.name}
184+
size="small"
185+
onDelete={() => handleDelete(obj.id)}
186+
deleteIcon={
187+
<Tooltip title={`Remove ${type}`}>
188+
<CloseIcon style={iconSmall} />
189+
</Tooltip>
190+
}
191+
/>
192+
))}
193+
{localSelectedData?.length > 1 && (
194+
<Typography
195+
onClick={() => setShowAllItems(!showAllItems)}
196+
sx={{
197+
cursor: 'pointer'
198+
}}
199+
>
200+
{showAllItems ? '(hide)' : `(+${localSelectedData?.length - 1})`}
201+
</Typography>
202+
)}
203+
</Box>
204+
</Box>
205+
);
206+
};
207+
208+
export default InputSearchField;

src/custom/InputSearchField/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import InputSearchField from './InputSearchField';
2+
3+
export { InputSearchField };

0 commit comments

Comments
 (0)