3870bceb6d
* 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
179 lines
5.3 KiB
TypeScript
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}
|
|
/>
|
|
);
|
|
})
|
|
}
|
|
/>
|
|
);
|
|
}
|