refactor: improve EventModal UI labels and visibility controls

- Rename 'Alarm' to 'Notification' for better clarity
- Rename 'Attendees' to 'Participants' with updated placeholder text
- Replace 'Visibility' dropdown with 'Visible to' toggle buttons (All/Participants)
- Remove CONFIDENTIAL option, keep only PUBLIC and PRIVATE
- Add icons (PublicIcon/LockIcon) with fixed sizing for toggle buttons
- Rename 'Show as' to 'Show me as'
- Fix ResponsiveDialog scroll bar positioning in expanded mode
- Update PeopleSearch placeholder to 'Start typing a name or email'
This commit is contained in:
lenhanphung
2025-10-01 19:16:39 +07:00
parent 1228f0a50e
commit 3261479874
4 changed files with 247 additions and 109 deletions
+2 -2
View File
@@ -102,8 +102,8 @@ export function PeopleSearch({
{...params} {...params}
error={!!error} error={!!error}
helperText={error} helperText={error}
placeholder="Search user" placeholder="Start typing a name or email"
label="Search user" label="Start typing a name or email"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter" && onToggleEventPreview) { if (e.key === "Enter" && onToggleEventPreview) {
e.preventDefault(); e.preventDefault();
+12 -1
View File
@@ -122,6 +122,11 @@ function ResponsiveDialog({
}; };
const baseContentSx: SxProps<Theme> = { const baseContentSx: SxProps<Theme> = {
width: "100%",
padding: isExpanded ? "16px" : undefined,
};
const contentWrapperSx: SxProps<Theme> = {
maxWidth: isExpanded ? expandedContentMaxWidth : "100%", maxWidth: isExpanded ? expandedContentMaxWidth : "100%",
margin: isExpanded ? "0 auto" : "0", margin: isExpanded ? "0 auto" : "0",
width: "100%", width: "100%",
@@ -160,7 +165,13 @@ function ResponsiveDialog({
]} ]}
{...dialogContentProps} {...dialogContentProps}
> >
<Stack spacing={currentSpacing}>{children}</Stack> {isExpanded ? (
<Stack spacing={currentSpacing} sx={contentWrapperSx}>
{children}
</Stack>
) : (
<Stack spacing={currentSpacing}>{children}</Stack>
)}
</DialogContent> </DialogContent>
{actions && <DialogActions>{actions}</DialogActions>} {actions && <DialogActions>{actions}</DialogActions>}
</Dialog> </Dialog>
+32 -44
View File
@@ -1,6 +1,5 @@
import { import {
FormControl, FormControl,
InputLabel,
Select, Select,
SelectChangeEvent, SelectChangeEvent,
MenuItem, MenuItem,
@@ -28,7 +27,6 @@ export default function RepeatEvent({
setRepetition: Function; setRepetition: Function;
isOwn?: boolean; isOwn?: boolean;
}) { }) {
const repetitionValues = ["day", "week", "month", "year"];
const days = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]; const days = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
const day = new Date(eventStart); const day = new Date(eventStart);
@@ -43,9 +41,11 @@ export default function RepeatEvent({
// keep endOption in sync if repetition changes from parent // keep endOption in sync if repetition changes from parent
useEffect(() => { useEffect(() => {
if (!endOption) { const newEndOption = getEndOption();
setEndOption(getEndOption()); if (endOption !== newEndOption) {
setEndOption(newEndOption);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [repetition.occurrences, repetition.endDate]); }, [repetition.occurrences, repetition.endDate]);
const handleDayChange = (day: string) => { const handleDayChange = (day: string) => {
@@ -57,37 +57,11 @@ export default function RepeatEvent({
}; };
return ( return (
<FormControl fullWidth margin="dense" size="small"> <Box>
<InputLabel id="repeat">Repetition</InputLabel> <Stack>
<Select
labelId="repeat"
value={repetition.freq ?? ""}
disabled={!isOwn}
label="Repetition"
onChange={(e: SelectChangeEvent) => {
if (e.target.value === "weekly") {
setRepetition({
...repetition,
freq: e.target.value,
selectedDays: [days[day.getDay() - 1]],
});
} else {
setRepetition({ ...repetition, freq: e.target.value });
}
}}
>
<MenuItem value={""}>No Repetition</MenuItem>
<MenuItem value={"daily"}>Repeat daily</MenuItem>
<MenuItem value={"weekly"}>Repeat weekly</MenuItem>
<MenuItem value={"monthly"}>Repeat monthly</MenuItem>
<MenuItem value={"yearly"}>Repeat yearly</MenuItem>
</Select>
{repetition.freq && (
<Stack>
{/* Interval */} {/* Interval */}
<Box display="flex" alignItems="center" gap={2} mb={2}> <Box display="flex" alignItems="center" gap={2} mb={2}>
<Typography>Interval:</Typography> <Typography>Repeat every</Typography>
<TextField <TextField
type="number" type="number"
value={repetition.interval ?? 1} value={repetition.interval ?? 1}
@@ -99,21 +73,36 @@ export default function RepeatEvent({
} }
size="small" size="small"
style={{ width: 80 }} style={{ width: 80 }}
inputProps={{ min: 1 }}
/> />
<Typography> <FormControl size="small" style={{ minWidth: 120 }}>
{ <Select
repetitionValues[ value={repetition.freq ?? "daily"}
repetitionValues.findIndex((el) => el === repetition.freq) onChange={(e: SelectChangeEvent) => {
] if (e.target.value === "weekly") {
} setRepetition({
</Typography> ...repetition,
freq: e.target.value,
selectedDays: [days[day.getDay() - 1]],
});
} else {
setRepetition({ ...repetition, freq: e.target.value });
}
}}
>
<MenuItem value={"daily"}>Day(s)</MenuItem>
<MenuItem value={"weekly"}>Week(s)</MenuItem>
<MenuItem value={"monthly"}>Month(s)</MenuItem>
<MenuItem value={"yearly"}>Year(s)</MenuItem>
</Select>
</FormControl>
</Box> </Box>
{/* Weekly selection */} {/* Weekly selection */}
{repetition.freq === "weekly" && ( {repetition.freq === "weekly" && (
<Box> <Box>
<Typography variant="body2" gutterBottom> <Typography variant="body2" gutterBottom>
On days: Repeat on:
</Typography> </Typography>
<FormGroup row> <FormGroup row>
{days.map((day) => ( {days.map((day) => (
@@ -136,7 +125,7 @@ export default function RepeatEvent({
{/* End options */} {/* End options */}
<Box> <Box>
<Typography variant="body2" gutterBottom style={{ marginTop: 16 }}> <Typography variant="body2" gutterBottom>
End: End:
</Typography> </Typography>
<RadioGroup <RadioGroup
@@ -222,7 +211,6 @@ export default function RepeatEvent({
</RadioGroup> </RadioGroup>
</Box> </Box>
</Stack> </Stack>
)} </Box>
</FormControl>
); );
} }
+201 -62
View File
@@ -11,8 +11,15 @@ import {
SelectChangeEvent, SelectChangeEvent,
TextField, TextField,
Typography, Typography,
ToggleButtonGroup,
ToggleButton,
} from "@mui/material"; } from "@mui/material";
import React, { useEffect, useState } from "react"; import {
Description as DescriptionIcon,
Public as PublicIcon,
Lock as LockIcon,
} from "@mui/icons-material";
import React, { useEffect, useState, useMemo } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { useAppDispatch, useAppSelector } from "../../app/hooks";
import AttendeeSelector from "../../components/Attendees/AttendeeSearch"; import AttendeeSelector from "../../components/Attendees/AttendeeSearch";
import { ResponsiveDialog } from "../../components/Dialog"; import { ResponsiveDialog } from "../../components/Dialog";
@@ -22,6 +29,7 @@ import { userAttendee } from "../User/userDataTypes";
import { CalendarEvent, RepetitionObject } from "./EventsTypes"; import { CalendarEvent, RepetitionObject } from "./EventsTypes";
import { createSelector } from "@reduxjs/toolkit"; import { createSelector } from "@reduxjs/toolkit";
import RepeatEvent from "../../components/Event/EventRepeat"; import RepeatEvent from "../../components/Event/EventRepeat";
import { TIMEZONES } from "../../utils/timezone-data";
// Helper component for field with label // Helper component for field with label
const FieldWithLabel = React.memo( const FieldWithLabel = React.memo(
@@ -112,7 +120,50 @@ function EventPopover({
const userPersonnalCalendars: Calendars[] = useAppSelector( const userPersonnalCalendars: Calendars[] = useAppSelector(
selectPersonnalCalendars selectPersonnalCalendars
); );
// Helper function to resolve timezone aliases
const resolveTimezone = (tzName: string): string => {
if (TIMEZONES.zones[tzName]) {
return tzName;
}
if (TIMEZONES.aliases[tzName]) {
return TIMEZONES.aliases[tzName].aliasTo;
}
return tzName;
};
const timezoneList = useMemo(() => {
const zones = Object.keys(TIMEZONES.zones).sort();
const browserTz = resolveTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
const getTimezoneOffset = (tzName: string): string => {
const resolvedTz = resolveTimezone(tzName);
const tzData = TIMEZONES.zones[resolvedTz];
if (!tzData) return "";
const icsMatch = tzData.ics.match(/TZOFFSETTO:([+-]\d{4})/);
if (!icsMatch) return "";
const offset = icsMatch[1];
const hours = parseInt(offset.slice(0, 3));
const minutes = parseInt(offset.slice(3));
if (minutes === 0) {
return `UTC${hours >= 0 ? '+' : ''}${hours}`;
}
return `UTC${hours >= 0 ? '+' : ''}${hours}:${Math.abs(minutes).toString().padStart(2, '0')}`;
};
return { zones, browserTz, getTimezoneOffset };
}, []);
const [showMore, setShowMore] = useState(false); const [showMore, setShowMore] = useState(false);
const [showDescription, setShowDescription] = useState(
event?.description ? true : false
);
const [showRepeat, setShowRepeat] = useState(
event?.repetition?.freq ? true : false
);
const [title, setTitle] = useState(event?.title ?? ""); const [title, setTitle] = useState(event?.title ?? "");
@@ -137,8 +188,10 @@ function EventPopover({
const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? ""); const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? "");
const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC"); const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC");
const [busy, setBusy] = useState(event?.transp ?? "OPAQUE"); const [busy, setBusy] = useState(event?.transp ?? "OPAQUE");
const [important, setImportant] = useState(false);
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const [timezone, setTimezone] = useState(
event?.timezone ? resolveTimezone(event.timezone) : timezoneList.browserTz
);
useEffect(() => { useEffect(() => {
if (selectedRange) { if (selectedRange) {
@@ -160,11 +213,15 @@ function EventPopover({
onClose({}, "backdropClick"); onClose({}, "backdropClick");
// Reset state // Reset state
setShowMore(false); setShowMore(false);
setShowDescription(false);
setShowRepeat(false);
setTitle(""); setTitle("");
setDescription(""); setDescription("");
setAttendees([]); setAttendees([]);
setLocation(""); setLocation("");
setCalendarid(0); setCalendarid(0);
setImportant(false);
setTimezone(timezoneList.browserTz);
}; };
const handleSave = async () => { const handleSave = async () => {
@@ -210,11 +267,15 @@ function EventPopover({
// Reset state // Reset state
setShowMore(false); setShowMore(false);
setShowDescription(false);
setShowRepeat(false);
setTitle(""); setTitle("");
setDescription(""); setDescription("");
setAttendees([]); setAttendees([]);
setLocation(""); setLocation("");
setCalendarid(0); setCalendarid(0);
setImportant(false);
setTimezone(timezoneList.browserTz);
// Save to API in background // Save to API in background
dispatch( dispatch(
@@ -254,46 +315,47 @@ function EventPopover({
<TextField <TextField
fullWidth fullWidth
label={!showMore ? "Title" : ""} label={!showMore ? "Title" : ""}
placeholder="Add title"
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
size="small" size="small"
margin="dense" margin="dense"
/> />
</FieldWithLabel> </FieldWithLabel>
<FieldWithLabel label="Description" isExpanded={showMore}>
<TextField <FieldWithLabel label=" " isExpanded={showMore}>
fullWidth <Box display="flex" gap={1} mb={1}>
label={!showMore ? "Description" : ""} <Button
value={description} startIcon={<DescriptionIcon />}
onChange={(e) => setDescription(e.target.value)} onClick={() => setShowDescription(true)}
size="small" size="small"
margin="dense" sx={{
multiline textTransform: "none",
rows={2} color: "text.secondary",
/> display: showDescription ? "none" : "flex",
</FieldWithLabel> }}
<FieldWithLabel label="Calendar" isExpanded={showMore}>
<FormControl fullWidth margin="dense" size="small">
{!showMore && (
<InputLabel id="calendar-select-label">Calendar</InputLabel>
)}
<Select
labelId="calendar-select-label"
value={calendarid.toString()}
label={!showMore ? "Calendar" : ""}
onChange={(e: SelectChangeEvent) =>
setCalendarid(Number(e.target.value))
}
> >
{Object.keys(userPersonnalCalendars).map((calendar, index) => ( Add description
<MenuItem key={index} value={index}> </Button>
{userPersonnalCalendars[index].name} </Box>
</MenuItem>
))}
</Select>
</FormControl>
</FieldWithLabel> </FieldWithLabel>
{showDescription && (
<FieldWithLabel label="Description" isExpanded={showMore}>
<TextField
fullWidth
label={!showMore ? "Description" : ""}
placeholder="Add description"
value={description}
onChange={(e) => setDescription(e.target.value)}
size="small"
margin="dense"
multiline
rows={2}
/>
</FieldWithLabel>
)}
<FieldWithLabel label="Date & Time" isExpanded={showMore}> <FieldWithLabel label="Date & Time" isExpanded={showMore}>
<Box display="flex" gap={2}> <Box display="flex" gap={2}>
<Box flexGrow={1}> <Box flexGrow={1}>
@@ -355,7 +417,17 @@ function EventPopover({
</Box> </Box>
</FieldWithLabel> </FieldWithLabel>
<FieldWithLabel label=" " isExpanded={showMore}> <FieldWithLabel label=" " isExpanded={showMore}>
<Box> <Box display="flex" gap={2} alignItems="center">
<FormControlLabel
control={
<Checkbox
checked={important}
onChange={() => setImportant(!important)}
/>
}
label="Mark as important"
sx={{ padding: "0 8px 0 0" }}
/>
<FormControlLabel <FormControlLabel
control={ control={
<Checkbox <Checkbox
@@ -390,39 +462,99 @@ function EventPopover({
label="All day" label="All day"
sx={{ padding: "0 8px 0 0" }} sx={{ padding: "0 8px 0 0" }}
/> />
<FormControlLabel
control={
<Checkbox
checked={showRepeat}
onChange={() => {
setShowRepeat(!showRepeat);
if (showRepeat) {
setRepetition({} as RepetitionObject);
} else {
setRepetition({
freq: "daily",
interval: 1,
} as RepetitionObject);
}
}}
/>
}
label="Repeat"
/>
<FormControl size="small" sx={{ width: 160 }}>
<Select
value={timezone}
onChange={(e: SelectChangeEvent) => setTimezone(e.target.value)}
displayEmpty
>
{timezoneList.zones.map((tz) => (
<MenuItem key={tz} value={tz}>
({timezoneList.getTimezoneOffset(tz)}) {tz.replace(/_/g, " ")}
</MenuItem>
))}
</Select>
</FormControl>
</Box> </Box>
</FieldWithLabel> </FieldWithLabel>
<FieldWithLabel label="Attendees" isExpanded={showMore}>
{showRepeat && (
<FieldWithLabel label=" " isExpanded={showMore}>
<RepeatEvent
repetition={repetition}
eventStart={selectedRange?.start ?? new Date()}
setRepetition={setRepetition}
/>
</FieldWithLabel>
)}
<FieldWithLabel label="Participants" isExpanded={showMore}>
<AttendeeSelector attendees={attendees} setAttendees={setAttendees} /> <AttendeeSelector attendees={attendees} setAttendees={setAttendees} />
</FieldWithLabel> </FieldWithLabel>
<FieldWithLabel label="Location" isExpanded={showMore}> <FieldWithLabel label="Location" isExpanded={showMore}>
<TextField <TextField
fullWidth fullWidth
label={!showMore ? "Location" : ""} label={!showMore ? "Location" : ""}
placeholder="Add location"
value={location} value={location}
onChange={(e) => setLocation(e.target.value)} onChange={(e) => setLocation(e.target.value)}
size="small" size="small"
margin="dense" margin="dense"
/> />
</FieldWithLabel> </FieldWithLabel>
<FieldWithLabel label="Calendar" isExpanded={showMore}>
<FormControl fullWidth margin="dense" size="small">
{!showMore && (
<InputLabel id="calendar-select-label">Calendar</InputLabel>
)}
<Select
labelId="calendar-select-label"
value={calendarid.toString()}
label={!showMore ? "Calendar" : ""}
displayEmpty
onChange={(e: SelectChangeEvent) =>
setCalendarid(Number(e.target.value))
}
>
{Object.keys(userPersonnalCalendars).map((calendar, index) => (
<MenuItem key={index} value={index}>
{userPersonnalCalendars[index].name}
</MenuItem>
))}
</Select>
</FormControl>
</FieldWithLabel>
{/* Extended options */} {/* Extended options */}
{showMore && ( {showMore && (
<> <>
<FieldWithLabel label="Repeat" isExpanded={showMore}> <FieldWithLabel label="Notification" isExpanded={showMore}>
<RepeatEvent
repetition={repetition}
eventStart={selectedRange?.start ?? new Date()}
setRepetition={setRepetition}
/>
</FieldWithLabel>
<FieldWithLabel label="Alarm" isExpanded={showMore}>
<FormControl fullWidth margin="dense" size="small"> <FormControl fullWidth margin="dense" size="small">
<Select <Select
labelId="alarm" labelId="notification"
value={alarm} value={alarm}
onChange={(e: SelectChangeEvent) => setAlarm(e.target.value)} onChange={(e: SelectChangeEvent) => setAlarm(e.target.value)}
> >
<MenuItem value={""}>No Alarm</MenuItem> <MenuItem value={""}>No Notification</MenuItem>
<MenuItem value={"-PT1M"}>1 minute</MenuItem> <MenuItem value={"-PT1M"}>1 minute</MenuItem>
<MenuItem value={"-PT5M"}>2 minutes</MenuItem> <MenuItem value={"-PT5M"}>2 minutes</MenuItem>
<MenuItem value={"-PT10M"}>10 minutes</MenuItem> <MenuItem value={"-PT10M"}>10 minutes</MenuItem>
@@ -439,22 +571,7 @@ function EventPopover({
</FormControl> </FormControl>
</FieldWithLabel> </FieldWithLabel>
<FieldWithLabel label="Visibility" isExpanded={showMore}> <FieldWithLabel label="Show me as" isExpanded={showMore}>
<FormControl fullWidth margin="dense" size="small">
<Select
labelId="Visibility"
value={eventClass}
onChange={(e: SelectChangeEvent) =>
setEventClass(e.target.value)
}
>
<MenuItem value={"PUBLIC"}>Public</MenuItem>
<MenuItem value={"CONFIDENTIAL"}>Show time only</MenuItem>
<MenuItem value={"PRIVATE"}>Private</MenuItem>
</Select>
</FormControl>
</FieldWithLabel>
<FieldWithLabel label="Show as" isExpanded={showMore}>
<FormControl fullWidth margin="dense" size="small"> <FormControl fullWidth margin="dense" size="small">
<Select <Select
labelId="busy" labelId="busy"
@@ -466,6 +583,28 @@ function EventPopover({
</Select> </Select>
</FormControl> </FormControl>
</FieldWithLabel> </FieldWithLabel>
<FieldWithLabel label="Visible to" isExpanded={showMore}>
<ToggleButtonGroup
value={eventClass}
exclusive
onChange={(e, newValue) => {
if (newValue !== null) {
setEventClass(newValue);
}
}}
size="small"
>
<ToggleButton value="PUBLIC" sx={{ width: '140px' }}>
<PublicIcon sx={{ mr: 1, fontSize: '16px' }} />
All
</ToggleButton>
<ToggleButton value="PRIVATE" sx={{ width: '140px' }}>
<LockIcon sx={{ mr: 1, fontSize: '16px' }} />
Participants
</ToggleButton>
</ToggleButtonGroup>
</FieldWithLabel>
</> </>
)} )}
</ResponsiveDialog> </ResponsiveDialog>