Bläddra i källkod

[Search] Add autocomplete to searchbox & add renderCell to searchresult

create_edit_user
cyril.tsui 2 månader sedan
förälder
incheckning
590ff27168
2 ändrade filer med 139 tillägg och 28 borttagningar
  1. +135
    -26
      src/components/SearchBox/SearchBox.tsx
  2. +4
    -2
      src/components/SearchResults/SearchResults.tsx

+ 135
- 26
src/components/SearchBox/SearchBox.tsx Visa fil

@@ -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<T extends string> {
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<T extends string> extends BaseCriterion<T> {
handleSelectionChange: (selectedOptions: T[]) => void;
}

// TODO: Add group
interface AutocompleteOptions {
value: string | number;
label: string;
group?: string;
}

interface AutocompleteCriterion<T extends string> extends BaseCriterion<T> {
type: "autocomplete";
options: AutocompleteOptions[];
multiple?: boolean;
noOptionsText?: string;
needAll?: boolean;
}

interface DateRangeCriterion<T extends string> extends BaseCriterion<T> {
type: "dateRange";
}

interface DateCriterion<T extends string> extends BaseCriterion<T> {
type: "date";
}

export type Criterion<T extends string> =
| TextCriterion<T>
| SelectCriterion<T>
| DateRangeCriterion<T>
| MultiSelectCriterion<T>;
| DateCriterion<T>
| MultiSelectCriterion<T>
| AutocompleteCriterion<T>;

interface Props<T extends string> {
criteria: Criterion<T>[];
@@ -71,11 +93,15 @@ function SearchBox<T extends string>({
onReset,
}: Props<T>) {
const { t } = useTranslation("common");
const defaultAll = { value: "All", label: t("All"), group: t("All") }
const defaultInputs = useMemo(
() =>
criteria.reduce<Record<T, string>>(
(acc, c) => {
return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" };
return {
...acc,
[c.paramName]: (c.type === "select" || c.type === "autocomplete" ? "All" : "")
};
},
{} as Record<T, string>,
),
@@ -99,6 +125,18 @@ function SearchBox<T extends string>({
};
}, []);

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 extends string>({
<Typography variant="overline">{t("Search Criteria")}</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
{criteria.map((c) => {
console.log(c.options)
return (
<Grid key={c.paramName} item xs={6}>
{c.type === "text" && (
@@ -141,13 +180,13 @@ function SearchBox<T extends string>({
/>
)}
{c.type === "multi-select" && (
<MultiSelect
label={c.label}
options={c?.options}
selectedValues={c.filterObj?.[c.paramName] ?? []}
onChange={c.handleSelectionChange}
isReset={isReset}
/>
<MultiSelect
label={c.label}
options={c?.options}
selectedValues={c.filterObj?.[c.paramName] ?? []}
onChange={c.handleSelectionChange}
isReset={isReset}
/>
)}
{c.type === "select" && (
<FormControl fullWidth>
@@ -166,6 +205,76 @@ function SearchBox<T extends string>({
</Select>
</FormControl>
)}
{c.type === "autocomplete" && (
<Autocomplete
groupBy={
c.options.every((option) => 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) => (
<React.Fragment key={`${params.key}-${params.group}`}>
<ListSubheader>{params.group}</ListSubheader>
{params.children}
</React.Fragment>
) : 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 (
<Chip
{...chipProps}
key={`${option.value}-${option.label}`}
label={option.label}
/>
);
})
: undefined
}
renderOption={(
params: React.HTMLAttributes<HTMLLIElement> & { key?: React.Key },
option,
{ selected },
) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { key, ...rest } = params;
return (
<MenuItem
{...rest}
disableRipple
value={option.value}
key={`${option.value}--${option.label}`}
>
{c.multiple && (
<Checkbox
disableRipple
key={`checkbox-${option.value}`}
checked={selected}
sx={{ transform: "translate(0)" }}
/>
)}
{option.label}
</MenuItem>
);
}}
renderInput={(params) => <TextField {...params} variant="outlined" label={c.label} />}
/>
)}
{c.type === "dateRange" && (
<LocalizationProvider
dateAdapter={AdapterDayjs}
@@ -197,20 +306,20 @@ function SearchBox<T extends string>({
</LocalizationProvider>
)}
{c.type === "date" && (
<LocalizationProvider
dateAdapter={AdapterDayjs}
// TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
adapterLocale="zh-hk"
>
<Box display="flex">
<FormControl fullWidth>
<DatePicker
label={c.label}
onChange={makeDateChangeHandler(c.paramName)}
/>
</FormControl>
</Box>
</LocalizationProvider>
<LocalizationProvider
dateAdapter={AdapterDayjs}
// TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
adapterLocale="zh-hk"
>
<Box display="flex">
<FormControl fullWidth>
<DatePicker
label={c.label}
onChange={makeDateChangeHandler(c.paramName)}
/>
</FormControl>
</Box>
</LocalizationProvider>
)}
</Grid>
);


+ 4
- 2
src/components/SearchResults/SearchResults.tsx Visa fil

@@ -30,6 +30,7 @@ interface BaseColumn<T extends ResultWithId> {
sx?: SxProps<Theme> | undefined;
style?: Partial<HTMLElement["style"]> & { [propName: string]: string };
type?: ColumnType;
renderCell?: (params: T) => React.ReactNode;
}

interface IconColumn<T extends ResultWithId> extends BaseColumn<T> {
@@ -272,8 +273,9 @@ function TabelCells<T extends ResultWithId>({
) :
isIntegerColumn(column) ? (
<>{integerFormatter.format(Number(item[columnName]))}</>
) : (
<>{item[columnName] as string}</>
) :
(
column.renderCell ? column.renderCell(item) : <>{item[columnName] as string}</>
)}
</TableCell>)
}


Laddar…
Avbryt
Spara