From 590ff271688d5e098ca3ec734491e5d51809879b Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Thu, 5 Jun 2025 19:16:33 +0800 Subject: [PATCH] [Search] Add autocomplete to searchbox & add renderCell to searchresult --- src/components/SearchBox/SearchBox.tsx | 161 +++++++++++++++--- .../SearchResults/SearchResults.tsx | 6 +- 2 files changed, 139 insertions(+), 28 deletions(-) diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index bbd7c41..2aea17a 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -4,7 +4,7 @@ import Grid from "@mui/material/Grid"; import Card from "@mui/material/Card"; import CardContent from "@mui/material/CardContent"; import Typography from "@mui/material/Typography"; -import React, { useCallback, useMemo, useState } from "react"; +import React, { SyntheticEvent, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import TextField from "@mui/material/TextField"; import FormControl from "@mui/material/FormControl"; @@ -20,15 +20,16 @@ import "dayjs/locale/zh-hk"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; -import { Box } from "@mui/material"; +import { Autocomplete, Box, Checkbox, Chip, ListSubheader } from "@mui/material"; import MultiSelect from "@/components/SearchBox/MultiSelect"; +import { intersectionWith } from "lodash"; interface BaseCriterion { label: string; label2?: string; paramName: T; paramName2?: T; - options?: T[] | string[]; + // options?: T[] | string[]; filterObj?: T; handleSelectionChange?: (selectedOptions: T[]) => void; } @@ -49,15 +50,36 @@ interface MultiSelectCriterion extends BaseCriterion { handleSelectionChange: (selectedOptions: T[]) => void; } +// TODO: Add group +interface AutocompleteOptions { + value: string | number; + label: string; + group?: string; +} + +interface AutocompleteCriterion extends BaseCriterion { + type: "autocomplete"; + options: AutocompleteOptions[]; + multiple?: boolean; + noOptionsText?: string; + needAll?: boolean; +} + interface DateRangeCriterion extends BaseCriterion { type: "dateRange"; } +interface DateCriterion extends BaseCriterion { + type: "date"; +} + export type Criterion = | TextCriterion | SelectCriterion | DateRangeCriterion - | MultiSelectCriterion; + | DateCriterion + | MultiSelectCriterion + | AutocompleteCriterion; interface Props { criteria: Criterion[]; @@ -71,11 +93,15 @@ function SearchBox({ onReset, }: Props) { const { t } = useTranslation("common"); + const defaultAll = { value: "All", label: t("All"), group: t("All") } const defaultInputs = useMemo( () => criteria.reduce>( (acc, c) => { - return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" }; + return { + ...acc, + [c.paramName]: (c.type === "select" || c.type === "autocomplete" ? "All" : "") + }; }, {} as Record, ), @@ -99,6 +125,18 @@ function SearchBox({ }; }, []); + const makeAutocompleteChangeHandler = useCallback((paramName: T, multiple: boolean) => { + return (e: SyntheticEvent, newValue: AutocompleteOptions | AutocompleteOptions[]) => { + if (multiple) { + const multiNewValue = newValue as AutocompleteOptions[]; + setInputs((i) => ({ ...i, [paramName]: multiNewValue.map(({ value }) => value) })); + } else { + const singleNewValue = newValue as AutocompleteOptions; + setInputs((i) => ({ ...i, [paramName]: singleNewValue.value })); + } + }; + }, []); + const makeDateChangeHandler = useCallback((paramName: T) => { return (e: any) => { setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") })); @@ -130,6 +168,7 @@ function SearchBox({ {t("Search Criteria")} {criteria.map((c) => { + console.log(c.options) return ( {c.type === "text" && ( @@ -141,13 +180,13 @@ function SearchBox({ /> )} {c.type === "multi-select" && ( - + )} {c.type === "select" && ( @@ -166,6 +205,76 @@ function SearchBox({ )} + {c.type === "autocomplete" && ( + option.group) + ? (option) => (option.group && option.group.trim() !== '' ? option.group : 'Ungrouped') + : undefined + } + multiple={Boolean(c.multiple)} + noOptionsText={c.noOptionsText ?? t("No options")} + disableClearable + fullWidth + value={c.multiple ? intersectionWith(c.options, inputs[c.paramName], (option, v) => { + return option.value === (v ?? "") + }) + : c.options.find((option) => option.value === inputs[c.paramName]) ?? defaultAll} + onChange={makeAutocompleteChangeHandler(c.paramName, Boolean(c.multiple))} + getOptionLabel={(option) => option.label} + options={[defaultAll, ...c.options]} + disableCloseOnSelect={Boolean(c.multiple)} + renderGroup={c.options.every((option) => option.group) ? (params) => ( + + {params.group} + {params.children} + + ) : undefined} + renderTags={ + c.multiple + ? (value, getTagProps) => + value.map((option, index) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { key, ...chipProps } = getTagProps({ index }); + return ( + + ); + }) + : undefined + } + renderOption={( + params: React.HTMLAttributes & { key?: React.Key }, + option, + { selected }, + ) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { key, ...rest } = params; + return ( + + {c.multiple && ( + + )} + {option.label} + + ); + }} + renderInput={(params) => } + /> + )} {c.type === "dateRange" && ( ({ )} {c.type === "date" && ( - - - - - - - + + + + + + + )} ); diff --git a/src/components/SearchResults/SearchResults.tsx b/src/components/SearchResults/SearchResults.tsx index 6a5a863..6f90893 100644 --- a/src/components/SearchResults/SearchResults.tsx +++ b/src/components/SearchResults/SearchResults.tsx @@ -30,6 +30,7 @@ interface BaseColumn { sx?: SxProps | undefined; style?: Partial & { [propName: string]: string }; type?: ColumnType; + renderCell?: (params: T) => React.ReactNode; } interface IconColumn extends BaseColumn { @@ -272,8 +273,9 @@ function TabelCells({ ) : isIntegerColumn(column) ? ( <>{integerFormatter.format(Number(item[columnName]))} - ) : ( - <>{item[columnName] as string} + ) : + ( + column.renderCell ? column.renderCell(item) : <>{item[columnName] as string} )} ) }