From 19f56f194f869a4a02e2f6943ef5a794e666d52c Mon Sep 17 00:00:00 2001 From: Amit Amrutiya Date: Tue, 5 Nov 2024 12:38:51 +0530 Subject: [PATCH 1/9] feat: create user serach field component Signed-off-by: Amit Amrutiya --- .../UserSearchField/UserSearchFieldInput.tsx | 325 ++++++++++++++++++ src/custom/UserSearchField/index.ts | 3 + 2 files changed, 328 insertions(+) create mode 100644 src/custom/UserSearchField/UserSearchFieldInput.tsx create mode 100644 src/custom/UserSearchField/index.ts diff --git a/src/custom/UserSearchField/UserSearchFieldInput.tsx b/src/custom/UserSearchField/UserSearchFieldInput.tsx new file mode 100644 index 00000000..cbced2b8 --- /dev/null +++ b/src/custom/UserSearchField/UserSearchFieldInput.tsx @@ -0,0 +1,325 @@ +import { Autocomplete, AutocompleteRenderInputParams } from '@mui/material'; +import { debounce } from 'lodash'; +import React, { SyntheticEvent, useMemo, useState } from 'react'; +import { + Avatar, + Box, + Checkbox, + Chip, + CircularProgress, + FormControlLabel, + FormGroup, + Grid, + TextField, + Tooltip, + Typography +} from '../../base'; +import { iconSmall } from '../../constants/iconsSizes'; +import { CloseIcon, PersonIcon } from '../../icons'; + +interface User { + user_id: string; + first_name: string; + last_name: string; + email: string; + avatar_url?: string; + deleted_at?: { Valid: boolean }; + deleted?: boolean; +} + +interface UserSearchFieldProps { + usersData: User[]; + setUsersData: React.Dispatch>; + label?: string; + setDisableSave?: (disable: boolean) => void; + handleNotifyPref?: () => void; + notifyUpdate?: boolean; + isCreate?: boolean; + searchType?: string; + disabled?: boolean; + org_id?: string; + currentUserData: User; + searchedUsers: User[]; + isUserSearchLoading: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fetchSearchedUsers: any; + usersSearch: string; + setUsersSearch: React.Dispatch>; +} + +const UserSearchField: React.FC = ({ + usersData, + setUsersData, + label, + setDisableSave, + handleNotifyPref, + notifyUpdate, + isCreate, + searchType, + disabled = false, + currentUserData, + searchedUsers, + isUserSearchLoading, + fetchSearchedUsers, + usersSearch, + setUsersSearch +}) => { + const [error, setError] = useState(''); + const [inputValue, setInputValue] = useState(''); + const [open, setOpen] = useState(false); + const [showAllUsers, setShowAllUsers] = useState(false); + const [hasInitialFocus, setHasInitialFocus] = useState(true); + // Combine current user with search results + const displayOptions = useMemo(() => { + if (!searchedUsers) return []; + + // Filter out current user from search results + const filteredResults = searchedUsers.filter( + (user: User) => user.user_id !== currentUserData?.user_id + ); + // Show only current user on initial focus + if (hasInitialFocus && !usersSearch && currentUserData) { + return [currentUserData]; + } + // If there's no search query, add current user at the top + if (!usersSearch && currentUserData) { + return [currentUserData, ...filteredResults]; + } + return filteredResults; + }, [searchedUsers, currentUserData, usersSearch, hasInitialFocus]); + + const fetchSuggestions = debounce((value: string) => { + setHasInitialFocus(false); + setUsersSearch(value); + fetchSearchedUsers(); + }, 300); + + const handleDelete = (email: string) => { + const usersDataSet = new Set(usersData); + usersDataSet.forEach((avatarObj: User) => { + if (avatarObj.email === email) { + usersDataSet.delete(avatarObj); + } + }); + setUsersData(Array.from(usersDataSet)); + if (setDisableSave) { + setDisableSave(false); + } + }; + + const handleAdd = (event: SyntheticEvent, value: User) => { + if (!value) return; + + setUsersData((prevData: User[]) => { + prevData = prevData || []; + const isDuplicate = prevData?.some((user) => user.user_id === value.user_id); + const isDeleted = value.deleted_at?.Valid === true; + + if (isDuplicate || isDeleted) { + setError(isDuplicate ? 'User already selected' : 'User does not exist'); + return prevData; + } + + setError(''); + return [...prevData, value]; + }); + + // Reset UI state after updating users + setInputValue(''); + setOpen(false); + setUsersSearch(''); + + if (setDisableSave) { + setDisableSave(false); + } + }; + + const handleInputChange = (event: SyntheticEvent, value: string) => { + if (value === '') { + setOpen(true); + setUsersSearch(''); + setHasInitialFocus(true); + } else { + const encodedValue = encodeURIComponent(value); + fetchSuggestions(encodedValue); + setError(''); + setOpen(true); + } + }; + + const handleFocus = () => { + setOpen(true); + if (!usersSearch) { + setHasInitialFocus(true); + } + }; + + const handleBlur = () => { + setOpen(false); + setUsersSearch(''); + // Reset initial focus state when field is blurred + setHasInitialFocus(true); + }; + + return ( + <> + x} + options={displayOptions} + disableClearable + includeInputInList + filterSelectedOptions + disableListWrap + disabled={disabled} + open={open} + loading={isUserSearchLoading} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + value={inputValue} + getOptionLabel={() => ''} + noOptionsText={isUserSearchLoading ? 'Loading...' : 'No users found'} + onChange={handleAdd} + onInputChange={handleInputChange} + isOptionEqualToValue={(option: User, value: User) => option.user_id === value.user_id} + clearOnBlur + onFocus={handleFocus} + onBlur={handleBlur} + renderInput={(params: AutocompleteRenderInputParams) => ( + + {isUserSearchLoading ? : null} + + ) + }} + /> + )} + renderOption={(props, option: User) => ( +
  • + + + + + {option.avatar_url ? '' : } + + + + + {option.deleted ? ( + + {option.email} (deleted) + + ) : ( + <> + + {option.first_name} {option.last_name} + + + {option.email} + + + )} + + +
  • + )} + /> + + {/* TODO: Remove dependancy of this checkbox in this component, it should be defined on parent component. We should keep this component reusable and should not add checkbox specific to some component */} + {!isCreate && ( + +
    + + } + label={`Notify ${searchType} of membership change`} + /> +
    +
    + )} + 0 ? '0.5rem' : '' + }} + > + {!showAllUsers && usersData?.length > 0 && ( + + {usersData[usersData.length - 1]?.avatar_url + ? '' + : usersData[usersData.length - 1]?.first_name?.charAt(0)} + + } + label={usersData[usersData.length - 1]?.email} + size="small" + onDelete={() => handleDelete(usersData[usersData.length - 1]?.email)} + deleteIcon={ + + + + } + /> + )} + {showAllUsers && + usersData?.map((avatarObj: User) => ( + + {avatarObj.avatar_url ? '' : avatarObj.first_name?.charAt(0)} + + } + label={avatarObj.email} + size="small" + onDelete={() => handleDelete(avatarObj.email)} + deleteIcon={ + + + + } + /> + ))} + {usersData?.length > 1 && ( + setShowAllUsers(!showAllUsers)} + sx={{ + cursor: 'pointer', + color: 'white', + fontWeight: '600', + '&:hover': { + color: 'black' + } + }} + > + {showAllUsers ? '(hide)' : `(+${usersData.length - 1})`} + + )} + + + ); +}; + +export default UserSearchField; diff --git a/src/custom/UserSearchField/index.ts b/src/custom/UserSearchField/index.ts new file mode 100644 index 00000000..7cf9a5ea --- /dev/null +++ b/src/custom/UserSearchField/index.ts @@ -0,0 +1,3 @@ +import UserSearchField from './UserSearchFieldInput'; + +export { UserSearchField }; From 42783be2869ba86d9650637faa2580f6f20d0bdb Mon Sep 17 00:00:00 2001 From: Amit Amrutiya Date: Tue, 5 Nov 2024 12:39:22 +0530 Subject: [PATCH 2/9] feat: add InputFieldSearch component for user search functionality Signed-off-by: Amit Amrutiya --- .../InputSearchField/InputSearchField.tsx | 189 ++++++++++++++++++ src/custom/InputSearchField/index.ts | 3 + src/custom/index.tsx | 2 + 3 files changed, 194 insertions(+) create mode 100644 src/custom/InputSearchField/InputSearchField.tsx create mode 100644 src/custom/InputSearchField/index.ts diff --git a/src/custom/InputSearchField/InputSearchField.tsx b/src/custom/InputSearchField/InputSearchField.tsx new file mode 100644 index 00000000..80d1b510 --- /dev/null +++ b/src/custom/InputSearchField/InputSearchField.tsx @@ -0,0 +1,189 @@ +import { Autocomplete } from '@mui/material'; +import React, { useEffect, useRef, useState } from 'react'; +import { Box, Chip, Grid, TextField, Tooltip, Typography } from '../../base'; +import { iconLarge, iconSmall } from '../../constants/iconsSizes'; +import { CloseIcon, OrgIcon } from '../../icons'; + +interface Option { + id: string; + name: string; +} + +interface InputFieldSearchProps { + defaultData?: Option[]; + label?: string; + fetchSuggestions: (value: string) => void; + setFilterData: (data: Option[]) => void; + isLoading: boolean; + type: string; + disabled?: boolean; +} + +const InputFieldSearch: React.FC = ({ + defaultData = [], + label, + fetchSuggestions, + setFilterData, + isLoading, + type, + disabled +}) => { + const [data, setData] = useState([]); + const [error, setError] = useState(''); + const [inputValue, setInputValue] = useState(''); + const [open, setOpen] = useState(false); + const [showAllUsers, setShowAllUsers] = useState(false); + const [selectedOption, setSelectedOption] = useState