Files
workavia-calendar-front/src/components/Attendees/PeopleSearch.tsx
T
lenhanphung 3870bceb6d feat: prepare project for cozy-ui migration (#157)
* feat: prepare project for cozy-ui migration

Build Configuration
- Add Stylus support via @rsbuild/plugin-stylus
- Add SVGR support via @rsbuild/plugin-svgr
- Update rsbuild.config.ts with custom configuration
- Add historyApiFallback for SPA routing

ESLint Configuration
- Create .eslintrc.json with react-app configuration
- Remove eslintConfig from package.json

CSS to Stylus Migration
Convert all 5 CSS files to Stylus (.styl) format:
  - src/App.css to src/App.styl
  - src/index.css to src/index.styl
  - src/components/Calendar/Calendar.css to src/components/Calendar/Calendar.styl
  - src/components/Calendar/CustomCalendar.css to src/components/Calendar/CustomCalendar.styl
  - src/components/Menubar/Menubar.css to src/components/Menubar/Menubar.styl
- Update all imports from .css to .styl
- Preserve all styling rules and values

MUI Styling Cleanup
- Replace all MUI sx props with standard style props across 15+ components
- Convert MUI spacing units (mr: 2 to marginRight: 16px)
- Convert MUI shorthand props (p: 2 to padding: 16px, mb: 1 to marginBottom: 8px)
- Remove MUI-specific syntax (&.Mui-checked selectors)
- Maintain exact visual appearance

FullCalendar Fixes
- Comment out problematic CSS rule in CustomCalendar.styl
- Fix display issues with timegrid slot labels

Files Modified
- Config: rsbuild.config.ts, .eslintrc.json, package.json
- Styling: 5 CSS to Stylus conversions, 15+ component sx to style updates
- Components: All components using MUI sx props updated

Development Workflow
- Continue using existing development commands
- Stylus files are now the source of truth for styling
- MUI components work with standard style props
- ESLint uses familiar react-app rules

Dependencies Added
- @rsbuild/plugin-stylus
- @rsbuild/plugin-svgr
2025-10-01 17:31:18 +02:00

179 lines
5.3 KiB
TypeScript

import CloseIcon from "@mui/icons-material/Close";
import Autocomplete from "@mui/material/Autocomplete";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import CardActions from "@mui/material/CardActions";
import CardContent from "@mui/material/CardContent";
import CardHeader from "@mui/material/CardHeader";
import CircularProgress from "@mui/material/CircularProgress";
import IconButton from "@mui/material/IconButton";
import ListItem from "@mui/material/ListItem";
import ListItemAvatar from "@mui/material/ListItemAvatar";
import ListItemText from "@mui/material/ListItemText";
import Modal from "@mui/material/Modal";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import { useState, useEffect } from "react";
import { searchUsers } from "../../features/User/userAPI";
import PeopleOutlineOutlinedIcon from "@mui/icons-material/PeopleOutlineOutlined";
import Chip from "@mui/material/Chip";
import { useTheme } from "@mui/material/styles";
export interface User {
email: string;
displayName: string;
avatarUrl: string;
openpaasId: string;
color?: string;
}
export function PeopleSearch({
selectedUsers,
onChange,
objectTypes,
disabled,
freeSolo,
onToggleEventPreview,
}: {
selectedUsers: User[];
onChange: Function;
objectTypes: string[];
disabled?: boolean;
freeSolo?: boolean;
onToggleEventPreview?: Function;
}) {
const [query, setQuery] = useState("");
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState<User[]>([]);
const isValidEmail = (email: string) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const [error, setError] = useState<string | null>(null);
const theme = useTheme();
useEffect(() => {
const delayDebounceFn = setTimeout(async () => {
if (query) {
setLoading(true);
const res = await searchUsers(query, objectTypes);
setOptions(res);
setLoading(false);
}
}, 300);
return () => clearTimeout(delayDebounceFn);
}, [query]);
return (
<Autocomplete
freeSolo={freeSolo}
multiple
options={options}
disabled={disabled}
loading={loading}
filterOptions={(x) => x}
fullWidth
getOptionLabel={(option) => {
if (typeof option === "object") {
return option.displayName || option.email;
} else {
return option;
}
}}
filterSelectedOptions
value={selectedUsers}
onInputChange={(event, value) => setQuery(value)}
onChange={(event, value) => {
const last = value[value.length - 1];
if (typeof last === "string" && !isValidEmail(last)) {
setError(`"${last}" is not a valid email address`);
return;
}
setError(null);
const mapped = value.map((v: any) =>
typeof v === "string" ? { email: v } : v
);
onChange(event, mapped);
}}
renderInput={(params) => (
<TextField
{...params}
error={!!error}
helperText={error}
placeholder="Search user"
label="Search user"
onKeyDown={(e) => {
if (e.key === "Enter" && onToggleEventPreview) {
e.preventDefault();
onToggleEventPreview();
}
}}
slotProps={{
input: {
...params.InputProps,
startAdornment: (
<>
<PeopleOutlineOutlinedIcon
style={{ marginRight: 8, color: "rgba(0, 0, 0, 0.54)" }}
/>
{params.InputProps.startAdornment}
</>
),
endAdornment: (
<>
{loading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{params.InputProps.endAdornment}
</>
),
},
}}
/>
)}
renderOption={(props, option) => {
if (selectedUsers.find((u) => u.email === option.email)) return;
return (
<ListItem
{...props}
key={option.email + option.displayName}
disableGutters
>
<ListItemAvatar>
<Avatar src={option.avatarUrl} alt={option.displayName} />
</ListItemAvatar>
<ListItemText
primary={option.displayName}
secondary={option.email}
/>
</ListItem>
);
}}
renderValue={(value, getTagProps) =>
value.map((option, index) => {
const isString = typeof option === "string";
const label = isString ? option : option.displayName || option.email;
const chipColor = isString
? theme.palette.grey[300]
: (option.color ?? theme.palette.grey[300]);
const textColor = theme.palette.getContrastText(chipColor);
return (
<Chip
{...getTagProps({ index })}
key={label}
style={{
backgroundColor: chipColor,
color: textColor,
}}
label={label}
/>
);
})
}
/>
);
}