update normal mode event createa update modal
This commit is contained in:
Generated
+50
-4
@@ -17,7 +17,7 @@
|
||||
"@fullcalendar/moment-timezone": "^6.1.19",
|
||||
"@fullcalendar/react": "^6.1.17",
|
||||
"@fullcalendar/timegrid": "^6.1.17",
|
||||
"@linagora/twake-mui": "1.1.4",
|
||||
"@linagora/twake-mui": "^1.1.7",
|
||||
"@lottiefiles/dotlottie-react": "^0.17.13",
|
||||
"@mui/icons-material": "^7.1.2",
|
||||
"@mui/material": "^7.1.2",
|
||||
@@ -83,6 +83,52 @@
|
||||
"node": "24.x"
|
||||
}
|
||||
},
|
||||
"../twake-ui/packages/twake-mui": {
|
||||
"name": "@linagora/twake-mui",
|
||||
"version": "1.1.4",
|
||||
"extraneous": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.19.1",
|
||||
"@types/react": "18.3.26",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"cozy-tsconfig": "1.8.1",
|
||||
"eslint": "8.35.0",
|
||||
"eslint-config-cozy-app": "6.7.1",
|
||||
"eslint-config-prettier": "8.6.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-jest": "27.9.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"prettier": "2.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "24.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/material": "^7.1.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": false
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
"version": "4.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
|
||||
@@ -3136,9 +3182,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@linagora/twake-mui": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://npm.pkg.github.com/download/@linagora/twake-mui/1.1.4/56321d9e10cd99550e751f15c34aaac6842442c1",
|
||||
"integrity": "sha512-L6e+6KgqFUxFY6avmk4UG4/bsdfzo6TxBkahUZlKxvCNEEikVe8z2Cxd0JtUUAWrPGZonTII9n2KhdFSNmNxvg==",
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://npm.pkg.github.com/download/@linagora/twake-mui/1.1.7/dd45fa10aeb5c7e6b6e89f59ff9d88c7d2f7e507",
|
||||
"integrity": "sha512-9ZRCeWUWObbIk9h2jg3N0fZlJ+QRBouXzhMNMC4rjQBgFvnCZxZHiMt6kGXQmOov8JsHFKSRhunwfNBYy1cv8Q==",
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
"@fullcalendar/moment-timezone": "^6.1.19",
|
||||
"@fullcalendar/react": "^6.1.17",
|
||||
"@fullcalendar/timegrid": "^6.1.17",
|
||||
"@linagora/twake-mui": "1.1.4",
|
||||
"@linagora/twake-mui": "^1.1.7",
|
||||
"@lottiefiles/dotlottie-react": "^0.17.13",
|
||||
"@mui/icons-material": "^7.1.2",
|
||||
"@mui/material": "^7.1.2",
|
||||
|
||||
@@ -12,6 +12,7 @@ export default function UserSearch({
|
||||
setAttendees,
|
||||
disabled,
|
||||
inputSlot,
|
||||
placeholder,
|
||||
}: {
|
||||
attendees: userAttendee[];
|
||||
setAttendees: Function;
|
||||
@@ -19,8 +20,9 @@ export default function UserSearch({
|
||||
inputSlot?: (
|
||||
params: ExtendedAutocompleteRenderInputParams
|
||||
) => React.ReactNode;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
const [selectedUsers, setSelectedUsers] = useState(
|
||||
const [selectedUsers, setSelectedUsers] = useState<User[]>(
|
||||
attendees.map((attendee) => ({
|
||||
email: attendee.cal_address,
|
||||
displayName: attendee.cn ?? "",
|
||||
@@ -44,6 +46,7 @@ export default function UserSearch({
|
||||
objectTypes={["user", "contact"]}
|
||||
disabled={disabled}
|
||||
inputSlot={inputSlot}
|
||||
placeholder={placeholder}
|
||||
onChange={(event: any, value: User[]) => {
|
||||
setAttendees(
|
||||
value.map((attendee: User) =>
|
||||
|
||||
@@ -4,7 +4,7 @@ A highly reusable dialog component that supports both normal and expanded (fulls
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Two Modes**: Normal popup (685px) and expanded fullscreen mode
|
||||
- ✅ **Two Modes**: Normal popup (570px) and expanded fullscreen mode
|
||||
- ✅ **Preserves Header**: Expanded mode doesn't cover app header (90px default)
|
||||
- ✅ **Clean Expanded View**: No backdrop/shadow in expanded mode for seamless integration
|
||||
- ✅ **Instant Transition**: No animation when expanding for immediate feedback
|
||||
@@ -62,7 +62,7 @@ function MyComponent() {
|
||||
| `actions` | `ReactNode` | - | Action buttons |
|
||||
| `isExpanded` | `boolean` | `false` | Toggle fullscreen mode |
|
||||
| `onExpandToggle` | `() => void` | - | Handler for back button click when expanded |
|
||||
| `normalMaxWidth` | `string` | `"685px"` | Max width in normal mode |
|
||||
| `normalMaxWidth` | `string` | `"570px"` | Max width in normal mode |
|
||||
| `expandedContentMaxWidth` | `string` | `"990px"` | Content container max-width in expanded mode |
|
||||
| `headerHeight` | `string` | `"90px"` | App header height to preserve |
|
||||
| `normalSpacing` | `number` | `2` | Stack spacing in normal mode (MUI spacing units: 1 = 8px) |
|
||||
@@ -160,7 +160,7 @@ For apps with different header heights:
|
||||
|
||||
### Normal Mode (`isExpanded={false}`)
|
||||
|
||||
- Dialog: max-width = `normalMaxWidth` (default 685px)
|
||||
- Dialog: max-width = `normalMaxWidth` (default 570px)
|
||||
- Content: 100% width
|
||||
- Height: auto (fits content)
|
||||
- Position: centered with 32px margin
|
||||
@@ -215,7 +215,7 @@ import { ResponsiveDialog } from "@/components/Dialog";
|
||||
1. **Use `isExpanded` for Show More/Less**: Toggle between modes for better UX
|
||||
2. **Provide `onExpandToggle` handler**: Required for back button functionality in expanded mode
|
||||
3. **Hide expand button in actions when expanded**: Back arrow in header provides collapse action
|
||||
4. **Keep `normalMaxWidth` reasonable**: 685px works well for forms
|
||||
4. **Keep `normalMaxWidth` reasonable**: 570px works well for forms
|
||||
5. **Center content in expanded mode**: Default 990px provides good reading width
|
||||
6. **Preserve header**: Default 90px - adjust via `headerHeight` prop if needed
|
||||
7. **MUI Stack handles spacing**: Children wrapped in Stack with configurable spacing prop - override with `normalSpacing`/`expandedSpacing` if needed
|
||||
|
||||
@@ -21,7 +21,7 @@ import React, { ReactNode } from "react";
|
||||
* ResponsiveDialog - A reusable dialog component that can switch between normal and expanded modes
|
||||
*
|
||||
* Features:
|
||||
* - Normal mode: Dialog with customizable max-width (default 685px)
|
||||
* - Normal mode: Dialog with customizable max-width (default 570px)
|
||||
* - Expanded mode: Full height dialog (excluding app header) with centered content container
|
||||
* - Fully customizable with sx props for Dialog, DialogTitle, and DialogContent
|
||||
* - Preserves app header visibility in expanded mode
|
||||
@@ -58,7 +58,7 @@ interface ResponsiveDialogProps extends Omit<
|
||||
isExpanded?: boolean;
|
||||
/** Callback when expand/collapse button is clicked (required if using isExpanded) */
|
||||
onExpandToggle?: () => void;
|
||||
/** Max width in normal mode (default: "685px") */
|
||||
/** Max width in normal mode (default: "570px") */
|
||||
normalMaxWidth?: string;
|
||||
/** Max width of content container in expanded mode (default: "990px") */
|
||||
expandedContentMaxWidth?: string;
|
||||
@@ -98,7 +98,7 @@ function ResponsiveDialog({
|
||||
actions,
|
||||
isExpanded = false,
|
||||
onExpandToggle,
|
||||
normalMaxWidth = "685px",
|
||||
normalMaxWidth = "570px",
|
||||
expandedContentMaxWidth = "990px",
|
||||
headerHeight = "90px",
|
||||
normalSpacing = 2,
|
||||
@@ -202,8 +202,9 @@ function ResponsiveDialog({
|
||||
onClick={onExpandToggle}
|
||||
aria-label="expand"
|
||||
size="small"
|
||||
sx={{ marginRight: 1 }}
|
||||
>
|
||||
<OpenInFullIcon />
|
||||
<OpenInFullIcon sx={{ padding: "2px" }} />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton onClick={onClose} aria-label="close" size="small">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box, Button, TextField } from "@linagora/twake-mui";
|
||||
import { Description as DescriptionIcon } from "@mui/icons-material";
|
||||
import { Box, Button, TextField, useTheme } from "@linagora/twake-mui";
|
||||
import { alpha } from "@mui/material/styles";
|
||||
import { Notes as NotesIcon } from "@mui/icons-material";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { FieldWithLabel } from "./components/FieldWithLabel";
|
||||
|
||||
@@ -28,21 +29,40 @@ export function AddDescButton({
|
||||
| "warning";
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
{!showDescription && (
|
||||
<FieldWithLabel label=" " isExpanded={showMore}>
|
||||
<Box display="flex" gap={1}>
|
||||
<FieldWithLabel
|
||||
label={showMore || showDescription ? t("event.form.description") : ""}
|
||||
isExpanded={showMore}
|
||||
>
|
||||
{!showMore ? (
|
||||
<Button
|
||||
startIcon={<DescriptionIcon />}
|
||||
startIcon={<NotesIcon />}
|
||||
onClick={() => setShowDescription(true)}
|
||||
size="medium"
|
||||
variant={buttonVariant}
|
||||
color={buttonColor}
|
||||
fullWidth
|
||||
sx={{
|
||||
justifyContent: "flex-start",
|
||||
borderRadius: "4px",
|
||||
color: alpha(theme.palette.grey[900], 0.9),
|
||||
}}
|
||||
>
|
||||
{t("event.form.addDescription")}
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<Box display="flex" gap={1}>
|
||||
<Button
|
||||
startIcon={<NotesIcon />}
|
||||
onClick={() => setShowDescription(true)}
|
||||
size="medium"
|
||||
variant={buttonVariant}
|
||||
color={buttonColor}
|
||||
>
|
||||
{t("event.form.addDescription")}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</FieldWithLabel>
|
||||
)}
|
||||
{showDescription && (
|
||||
|
||||
@@ -20,12 +20,16 @@ import {
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@linagora/twake-mui";
|
||||
import { alpha } from "@mui/material/styles";
|
||||
import {
|
||||
Close as DeleteIcon,
|
||||
ContentCopy as CopyIcon,
|
||||
Public as PublicIcon,
|
||||
LocationOn as LocationIcon,
|
||||
} from "@mui/icons-material";
|
||||
import SquareRoundedIcon from "@mui/icons-material/SquareRounded";
|
||||
import LockOutlineIcon from "@mui/icons-material/LockOutline";
|
||||
import React from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
@@ -35,6 +39,7 @@ import { SnackbarAlert } from "../Loading/SnackBarAlert";
|
||||
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
|
||||
import { AddDescButton } from "./AddDescButton";
|
||||
import { DateTimeFields } from "./components/DateTimeFields";
|
||||
import { DateTimeSummary } from "./components/DateTimeSummary";
|
||||
import { FieldWithLabel } from "./components/FieldWithLabel";
|
||||
import RepeatEvent from "./EventRepeat";
|
||||
import { useAllDayToggle } from "./hooks/useAllDayToggle";
|
||||
@@ -156,6 +161,7 @@ export default function EventFormFields({
|
||||
onHasEndDateChangedChange,
|
||||
}: EventFormFieldsProps) {
|
||||
const { t } = useI18n();
|
||||
const theme = useTheme();
|
||||
|
||||
// Internal state for 4 separate fields
|
||||
const [startDate, setStartDate] = React.useState("");
|
||||
@@ -165,11 +171,28 @@ export default function EventFormFields({
|
||||
|
||||
// Track if user has manually changed end date in extended mode
|
||||
const [hasEndDateChanged, setHasEndDateChanged] = React.useState(false);
|
||||
|
||||
// Track if user has clicked on datetime clickable section in normal mode
|
||||
// Once clicked, normal mode will always show full fields until modal closes
|
||||
const [hasClickedDateTimeSection, setHasClickedDateTimeSection] = React.useState(false);
|
||||
|
||||
// Track if user has clicked on location section in normal mode
|
||||
const [hasClickedLocationSection, setHasClickedLocationSection] = React.useState(false);
|
||||
|
||||
// Track if user has clicked on calendar section in normal mode
|
||||
const [hasClickedCalendarSection, setHasClickedCalendarSection] = React.useState(false);
|
||||
|
||||
// Track if user has clicked on video meeting section in normal mode
|
||||
const [hasClickedVideoMeetingSection, setHasClickedVideoMeetingSection] = React.useState(false);
|
||||
|
||||
// Reset hasEndDateChanged when modal closes
|
||||
// Reset hasEndDateChanged and hasClickedDateTimeSection when modal closes
|
||||
React.useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setHasEndDateChanged(false);
|
||||
setHasClickedDateTimeSection(false);
|
||||
setHasClickedLocationSection(false);
|
||||
setHasClickedCalendarSection(false);
|
||||
setHasClickedVideoMeetingSection(false);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
@@ -255,6 +278,7 @@ export default function EventFormFields({
|
||||
prevShowMoreRef.current = showMore;
|
||||
}, [showMore, isOpen]);
|
||||
|
||||
|
||||
// Sync start prop to startDate/startTime
|
||||
React.useEffect(() => {
|
||||
if (start) {
|
||||
@@ -389,6 +413,12 @@ export default function EventFormFields({
|
||||
setDescription(updatedDescription);
|
||||
setHasVideoConference(true);
|
||||
setMeetingLink(newMeetingLink);
|
||||
// Open description field when adding video conference
|
||||
setShowDescription(true);
|
||||
// Track click on video meeting section in normal mode
|
||||
if (!showMore) {
|
||||
setHasClickedVideoMeetingSection(true);
|
||||
}
|
||||
};
|
||||
|
||||
const [openToast, setOpenToast] = React.useState(false);
|
||||
@@ -421,7 +451,10 @@ export default function EventFormFields({
|
||||
|
||||
return (
|
||||
<>
|
||||
<FieldWithLabel label={t("event.form.title")} isExpanded={showMore}>
|
||||
<FieldWithLabel
|
||||
label={showMore ? t("event.form.title") : ""}
|
||||
isExpanded={showMore}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
label=""
|
||||
@@ -437,6 +470,224 @@ export default function EventFormFields({
|
||||
/>
|
||||
</FieldWithLabel>
|
||||
|
||||
<FieldWithLabel
|
||||
label={
|
||||
!showMore && !hasClickedDateTimeSection ? "" : t("event.form.dateTime")
|
||||
}
|
||||
isExpanded={showMore}
|
||||
>
|
||||
{!showMore && !hasClickedDateTimeSection ? (
|
||||
<DateTimeSummary
|
||||
startDate={startDate}
|
||||
startTime={startTime}
|
||||
endDate={endDate}
|
||||
endTime={endTime}
|
||||
allday={allday}
|
||||
timezone={timezone}
|
||||
repetition={repetition}
|
||||
showEndDate={
|
||||
allday ||
|
||||
(hasEndDateChanged && startDate !== endDate) ||
|
||||
(!allday && startDate !== endDate)
|
||||
}
|
||||
onClick={() => setHasClickedDateTimeSection(true)}
|
||||
/>
|
||||
) : (
|
||||
<DateTimeFields
|
||||
startDate={startDate}
|
||||
startTime={startTime}
|
||||
endDate={endDate}
|
||||
endTime={endTime}
|
||||
allday={allday}
|
||||
showMore={showMore}
|
||||
hasEndDateChanged={hasEndDateChanged}
|
||||
validation={validation}
|
||||
onStartDateChange={handleStartDateChange}
|
||||
onStartTimeChange={handleStartTimeChange}
|
||||
onEndDateChange={handleEndDateChange}
|
||||
onEndTimeChange={handleEndTimeChange}
|
||||
showEndDate={
|
||||
showMore ||
|
||||
allday ||
|
||||
(hasEndDateChanged && startDate !== endDate) ||
|
||||
(!showMore && !allday && startDate !== endDate)
|
||||
}
|
||||
onToggleEndDate={() => {}}
|
||||
/>
|
||||
)}
|
||||
</FieldWithLabel>
|
||||
|
||||
{!(!showMore && !hasClickedDateTimeSection) && (
|
||||
<FieldWithLabel label=" " isExpanded={showMore}>
|
||||
<Box display="flex" gap={2} alignItems="center">
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox checked={allday} onChange={handleAllDayToggle} />
|
||||
}
|
||||
label={
|
||||
<Typography variant="h6">{t("event.form.allDay")}</Typography>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={
|
||||
showRepeat || (typeOfAction === "solo" && !!repetition?.freq)
|
||||
}
|
||||
disabled={typeOfAction === "solo"}
|
||||
onChange={() => {
|
||||
const newShowRepeat = !showRepeat;
|
||||
setShowRepeat(newShowRepeat);
|
||||
if (newShowRepeat) {
|
||||
setRepetition({
|
||||
freq: "daily",
|
||||
interval: 1,
|
||||
occurrences: 0,
|
||||
endDate: "",
|
||||
byday: null,
|
||||
} as RepetitionObject);
|
||||
} else {
|
||||
setRepetition({
|
||||
freq: "",
|
||||
interval: 1,
|
||||
occurrences: 0,
|
||||
endDate: "",
|
||||
byday: null,
|
||||
} as RepetitionObject);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="h6">{t("event.form.repeat")}</Typography>
|
||||
}
|
||||
/>
|
||||
<TimezoneAutocomplete
|
||||
value={timezone}
|
||||
onChange={setTimezone}
|
||||
zones={timezoneList.zones}
|
||||
getTimezoneOffset={(tzName: string) =>
|
||||
timezoneList.getTimezoneOffset(tzName, new Date(start))
|
||||
}
|
||||
showIcon={false}
|
||||
width={240}
|
||||
size="small"
|
||||
placeholder={t("event.form.timezonePlaceholder")}
|
||||
/>
|
||||
</Box>
|
||||
</FieldWithLabel>
|
||||
)}
|
||||
|
||||
{!(!showMore && !hasClickedDateTimeSection) &&
|
||||
(showRepeat || (typeOfAction === "solo" && repetition?.freq)) && (
|
||||
<FieldWithLabel label=" " isExpanded={showMore}>
|
||||
<RepeatEvent
|
||||
repetition={repetition}
|
||||
eventStart={new Date(start)}
|
||||
setRepetition={setRepetition}
|
||||
isOwn={typeOfAction !== "solo"}
|
||||
/>
|
||||
</FieldWithLabel>
|
||||
)}
|
||||
|
||||
<FieldWithLabel label="" isExpanded={showMore}>
|
||||
<AttendeeSelector
|
||||
attendees={attendees}
|
||||
setAttendees={setAttendees}
|
||||
placeholder="Add guests"
|
||||
inputSlot={(params) => <TextField {...params} size="small" />}
|
||||
/>
|
||||
</FieldWithLabel>
|
||||
|
||||
<FieldWithLabel
|
||||
label={showMore ? t("event.form.videoMeeting") : ""}
|
||||
isExpanded={showMore}
|
||||
sx={
|
||||
hasClickedVideoMeetingSection && !showMore
|
||||
? {
|
||||
marginTop: "0 !important",
|
||||
"& > .MuiBox-root": {
|
||||
marginTop: "0 !important",
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{!showMore ? (
|
||||
<Button
|
||||
startIcon={
|
||||
<img src={iconCamera} alt="camera" width={24} height={24} />
|
||||
}
|
||||
onClick={handleAddVideoConference}
|
||||
fullWidth
|
||||
sx={{
|
||||
justifyContent: "flex-start",
|
||||
display: hasVideoConference ? "none" : "flex",
|
||||
borderRadius: "4px",
|
||||
color: alpha(theme.palette.grey[900], 0.9),
|
||||
}}
|
||||
>
|
||||
{t("event.form.addVisioConference")}
|
||||
</Button>
|
||||
) : (
|
||||
<Box display="flex" gap={1} alignItems="center">
|
||||
<Button
|
||||
startIcon={
|
||||
<img src={iconCamera} alt="camera" width={24} height={24} />
|
||||
}
|
||||
onClick={handleAddVideoConference}
|
||||
size="medium"
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{
|
||||
borderRadius: "4px",
|
||||
display: hasVideoConference ? "none" : "flex",
|
||||
}}
|
||||
>
|
||||
{t("event.form.addVisioConference")}
|
||||
</Button>
|
||||
|
||||
{hasVideoConference && meetingLink && (
|
||||
<>
|
||||
<Button
|
||||
startIcon={
|
||||
<img src={iconCamera} alt="camera" width={24} height={24} />
|
||||
}
|
||||
onClick={() => window.open(meetingLink, "_blank")}
|
||||
size="medium"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{
|
||||
borderRadius: "4px",
|
||||
mr: 1,
|
||||
}}
|
||||
>
|
||||
{t("event.form.joinVisioConference")}
|
||||
</Button>
|
||||
<IconButton
|
||||
onClick={handleCopyMeetingLink}
|
||||
size="small"
|
||||
sx={{ color: "primary.main" }}
|
||||
aria-label={t("event.form.copyMeetingLink")}
|
||||
title={t("event.form.copyMeetingLink")}
|
||||
>
|
||||
<CopyIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleDeleteVideoConference}
|
||||
size="small"
|
||||
sx={{ color: "error.main" }}
|
||||
aria-label={t("event.form.removeVideoConference")}
|
||||
title={t("event.form.removeVideoConference")}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</FieldWithLabel>
|
||||
|
||||
<AddDescButton
|
||||
showDescription={showDescription}
|
||||
setShowDescription={setShowDescription}
|
||||
@@ -447,199 +698,88 @@ export default function EventFormFields({
|
||||
buttonColor="secondary"
|
||||
/>
|
||||
|
||||
<FieldWithLabel label={t("event.form.dateTime")} isExpanded={showMore}>
|
||||
<DateTimeFields
|
||||
startDate={startDate}
|
||||
startTime={startTime}
|
||||
endDate={endDate}
|
||||
endTime={endTime}
|
||||
allday={allday}
|
||||
showMore={showMore}
|
||||
hasEndDateChanged={hasEndDateChanged}
|
||||
validation={validation}
|
||||
onStartDateChange={handleStartDateChange}
|
||||
onStartTimeChange={handleStartTimeChange}
|
||||
onEndDateChange={handleEndDateChange}
|
||||
onEndTimeChange={handleEndTimeChange}
|
||||
showEndDate={
|
||||
showMore ||
|
||||
allday ||
|
||||
(hasEndDateChanged && startDate !== endDate) ||
|
||||
(!showMore && !allday && startDate !== endDate)
|
||||
}
|
||||
onToggleEndDate={() => {}}
|
||||
/>
|
||||
<FieldWithLabel
|
||||
label={
|
||||
showMore || hasClickedLocationSection
|
||||
? t("event.form.location")
|
||||
: ""
|
||||
}
|
||||
isExpanded={showMore}
|
||||
>
|
||||
{!showMore && !hasClickedLocationSection ? (
|
||||
<Button
|
||||
startIcon={<LocationIcon />}
|
||||
onClick={() => setHasClickedLocationSection(true)}
|
||||
fullWidth
|
||||
sx={{
|
||||
justifyContent: "flex-start",
|
||||
borderRadius: "4px",
|
||||
color: alpha(theme.palette.grey[900], 0.9),
|
||||
}}
|
||||
>
|
||||
{location || t("event.form.locationPlaceholder")}
|
||||
</Button>
|
||||
) : (
|
||||
<TextField
|
||||
fullWidth
|
||||
label=""
|
||||
inputProps={{ "aria-label": t("event.form.location") }}
|
||||
placeholder={t("event.form.locationPlaceholder")}
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
size="small"
|
||||
margin="dense"
|
||||
/>
|
||||
)}
|
||||
</FieldWithLabel>
|
||||
|
||||
<FieldWithLabel label=" " isExpanded={showMore}>
|
||||
<Box display="flex" gap={2} alignItems="center">
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox checked={allday} onChange={handleAllDayToggle} />
|
||||
}
|
||||
label={
|
||||
<Typography variant="h6">{t("event.form.allDay")}</Typography>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={
|
||||
showRepeat || (typeOfAction === "solo" && !!repetition?.freq)
|
||||
}
|
||||
disabled={typeOfAction === "solo"}
|
||||
onChange={() => {
|
||||
const newShowRepeat = !showRepeat;
|
||||
setShowRepeat(newShowRepeat);
|
||||
if (newShowRepeat) {
|
||||
setRepetition({
|
||||
freq: "daily",
|
||||
interval: 1,
|
||||
occurrences: 0,
|
||||
endDate: "",
|
||||
byday: null,
|
||||
} as RepetitionObject);
|
||||
} else {
|
||||
setRepetition({
|
||||
freq: "",
|
||||
interval: 1,
|
||||
occurrences: 0,
|
||||
endDate: "",
|
||||
byday: null,
|
||||
} as RepetitionObject);
|
||||
}
|
||||
<FieldWithLabel
|
||||
label={
|
||||
showMore || hasClickedCalendarSection
|
||||
? t("event.form.calendar")
|
||||
: ""
|
||||
}
|
||||
isExpanded={showMore}
|
||||
>
|
||||
{!showMore && !hasClickedCalendarSection ? (
|
||||
<Button
|
||||
startIcon={
|
||||
<SquareRoundedIcon
|
||||
sx={{
|
||||
color:
|
||||
userPersonalCalendars.find((cal) => cal.id === calendarid)
|
||||
?.color?.light ?? "#3788D8",
|
||||
width: 24,
|
||||
height: 24,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="h6">{t("event.form.repeat")}</Typography>
|
||||
}
|
||||
/>
|
||||
<TimezoneAutocomplete
|
||||
value={timezone}
|
||||
onChange={setTimezone}
|
||||
zones={timezoneList.zones}
|
||||
getTimezoneOffset={(tzName: string) =>
|
||||
timezoneList.getTimezoneOffset(tzName, new Date(start))
|
||||
}
|
||||
showIcon={false}
|
||||
width={240}
|
||||
size="small"
|
||||
placeholder={t("event.form.timezonePlaceholder")}
|
||||
/>
|
||||
</Box>
|
||||
</FieldWithLabel>
|
||||
|
||||
{(showRepeat || (typeOfAction === "solo" && repetition?.freq)) && (
|
||||
<FieldWithLabel label=" " isExpanded={showMore}>
|
||||
<RepeatEvent
|
||||
repetition={repetition}
|
||||
eventStart={new Date(start)}
|
||||
setRepetition={setRepetition}
|
||||
isOwn={typeOfAction !== "solo"}
|
||||
/>
|
||||
</FieldWithLabel>
|
||||
)}
|
||||
|
||||
<FieldWithLabel
|
||||
label={t("event.form.participants")}
|
||||
isExpanded={showMore}
|
||||
>
|
||||
<AttendeeSelector
|
||||
attendees={attendees}
|
||||
setAttendees={setAttendees}
|
||||
inputSlot={(params) => <TextField {...params} size="small" />}
|
||||
/>
|
||||
</FieldWithLabel>
|
||||
|
||||
<FieldWithLabel
|
||||
label={t("event.form.videoMeeting")}
|
||||
isExpanded={showMore}
|
||||
>
|
||||
<Box display="flex" gap={1} alignItems="center">
|
||||
<Button
|
||||
startIcon={
|
||||
<img src={iconCamera} alt="camera" width={24} height={24} />
|
||||
}
|
||||
onClick={handleAddVideoConference}
|
||||
size="medium"
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => setHasClickedCalendarSection(true)}
|
||||
fullWidth
|
||||
sx={{
|
||||
justifyContent: "flex-start",
|
||||
borderRadius: "4px",
|
||||
display: hasVideoConference ? "none" : "flex",
|
||||
color: alpha(theme.palette.grey[900], 0.9),
|
||||
}}
|
||||
>
|
||||
{t("event.form.addVisioConference")}
|
||||
{userPersonalCalendars.find((cal) => cal.id === calendarid)?.name ||
|
||||
t("event.form.calendar")}
|
||||
</Button>
|
||||
|
||||
{hasVideoConference && meetingLink && (
|
||||
<>
|
||||
<Button
|
||||
startIcon={
|
||||
<img src={iconCamera} alt="camera" width={24} height={24} />
|
||||
}
|
||||
onClick={() => window.open(meetingLink, "_blank")}
|
||||
size="medium"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{
|
||||
borderRadius: "4px",
|
||||
mr: 1,
|
||||
}}
|
||||
>
|
||||
{t("event.form.joinVisioConference")}
|
||||
</Button>
|
||||
<IconButton
|
||||
onClick={handleCopyMeetingLink}
|
||||
size="small"
|
||||
sx={{ color: "primary.main" }}
|
||||
aria-label={t("event.form.copyMeetingLink")}
|
||||
title={t("event.form.copyMeetingLink")}
|
||||
>
|
||||
<CopyIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleDeleteVideoConference}
|
||||
size="small"
|
||||
sx={{ color: "error.main" }}
|
||||
aria-label={t("event.form.removeVideoConference")}
|
||||
title={t("event.form.removeVideoConference")}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</FieldWithLabel>
|
||||
|
||||
<FieldWithLabel label={t("event.form.location")} isExpanded={showMore}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label=""
|
||||
inputProps={{ "aria-label": t("event.form.location") }}
|
||||
placeholder={t("event.form.locationPlaceholder")}
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
size="small"
|
||||
margin="dense"
|
||||
/>
|
||||
</FieldWithLabel>
|
||||
|
||||
<FieldWithLabel label={t("event.form.calendar")} isExpanded={showMore}>
|
||||
<FormControl fullWidth margin="dense" size="small">
|
||||
<Select
|
||||
value={calendarid ?? ""}
|
||||
label=""
|
||||
SelectDisplayProps={{ "aria-label": t("event.form.calendar") }}
|
||||
displayEmpty
|
||||
onChange={(e: SelectChangeEvent) =>
|
||||
handleCalendarChange(e.target.value)
|
||||
}
|
||||
>
|
||||
{CalendarItemList(userPersonalCalendars)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
) : (
|
||||
<FormControl fullWidth margin="dense" size="small">
|
||||
<Select
|
||||
value={calendarid ?? ""}
|
||||
label=""
|
||||
SelectDisplayProps={{ "aria-label": t("event.form.calendar") }}
|
||||
displayEmpty
|
||||
onChange={(e: SelectChangeEvent) =>
|
||||
handleCalendarChange(e.target.value)
|
||||
}
|
||||
>
|
||||
{CalendarItemList(userPersonalCalendars)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
</FieldWithLabel>
|
||||
|
||||
{showMore && (
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import { Box, Typography, useTheme } from "@linagora/twake-mui";
|
||||
import { alpha } from "@mui/material/styles";
|
||||
import AccessTimeIcon from "@mui/icons-material/AccessTime";
|
||||
import dayjs from "dayjs";
|
||||
import React from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { getTimezoneOffset } from "../../Calendar/TimezoneSelector";
|
||||
import { RepetitionObject } from "@/features/Events/EventsTypes";
|
||||
import { LONG_DATE_FORMAT } from "../utils/dateTimeFormatters";
|
||||
import { toDateTime } from "../utils/dateTimeHelpers";
|
||||
|
||||
interface DateTimeSummaryProps {
|
||||
startDate: string;
|
||||
startTime: string;
|
||||
endDate: string;
|
||||
endTime: string;
|
||||
allday: boolean;
|
||||
timezone: string;
|
||||
repetition: RepetitionObject;
|
||||
showEndDate: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const DateTimeSummary: React.FC<DateTimeSummaryProps> = ({
|
||||
startDate,
|
||||
startTime,
|
||||
endDate,
|
||||
endTime,
|
||||
allday,
|
||||
timezone,
|
||||
repetition,
|
||||
showEndDate,
|
||||
onClick,
|
||||
}) => {
|
||||
const { t, lang } = useI18n();
|
||||
const theme = useTheme();
|
||||
|
||||
// Format date: "Wednesday, January 28, 2026"
|
||||
const formatDate = (dateStr: string): string => {
|
||||
if (!dateStr) return "";
|
||||
const date = dayjs(dateStr);
|
||||
return date.format(LONG_DATE_FORMAT);
|
||||
};
|
||||
|
||||
// Format time: "1:00pm - 2:00pm"
|
||||
const formatTime = (startTimeStr: string, endTimeStr: string): string => {
|
||||
if (allday || !startTimeStr || !endTimeStr) return "";
|
||||
|
||||
const format12Hour = (timeStr: string): string => {
|
||||
const [hours, minutes] = timeStr.split(":").map(Number);
|
||||
const hour12 = hours % 12 || 12;
|
||||
const ampm = hours >= 12 ? "pm" : "am";
|
||||
const mins = minutes === 0 ? "" : `:${minutes.toString().padStart(2, "0")}`;
|
||||
return `${hour12}${mins}${ampm}`;
|
||||
};
|
||||
|
||||
const start = format12Hour(startTimeStr);
|
||||
const end = format12Hour(endTimeStr);
|
||||
return `${start} - ${end}`;
|
||||
};
|
||||
|
||||
// Format timezone: "(UTC+2) Paris"
|
||||
const formatTimezone = (tz: string): string => {
|
||||
if (!tz) return "";
|
||||
try {
|
||||
const offset = getTimezoneOffset(tz, new Date());
|
||||
const tzName = tz.replace(/_/g, " ");
|
||||
return `(${offset}) ${tzName}`;
|
||||
} catch (error) {
|
||||
return tz.replace(/_/g, " ");
|
||||
}
|
||||
};
|
||||
|
||||
// Format repeat: "Doesn't repeat" or repeat info
|
||||
const formatRepeat = (rep: RepetitionObject): string => {
|
||||
if (!rep || !rep.freq) {
|
||||
return "Doesn't repeat";
|
||||
}
|
||||
|
||||
const interval = rep.interval || 1;
|
||||
const freqMap: { [key: string]: string } = {
|
||||
daily: t("event.repeat.frequency.days"),
|
||||
weekly: t("event.repeat.frequency.weeks"),
|
||||
monthly: t("event.repeat.frequency.months"),
|
||||
yearly: t("event.repeat.frequency.years"),
|
||||
};
|
||||
|
||||
const freqText = freqMap[rep.freq] || rep.freq;
|
||||
|
||||
if (interval === 1) {
|
||||
return `Every ${freqText}`;
|
||||
}
|
||||
|
||||
return `Every ${interval} ${freqText}`;
|
||||
};
|
||||
|
||||
// Format date text: show both start and end date if showEndDate is true
|
||||
const formatDateText = (): string => {
|
||||
if (showEndDate && endDate && endDate !== startDate) {
|
||||
const startDateText = formatDate(startDate);
|
||||
const endDateText = formatDate(endDate);
|
||||
return `${startDateText} - ${endDateText}`;
|
||||
}
|
||||
return formatDate(startDate);
|
||||
};
|
||||
|
||||
const dateText = formatDateText();
|
||||
const timeText = formatTime(startTime, endTime);
|
||||
const timezoneText = formatTimezone(timezone);
|
||||
const repeatText = formatRepeat(repetition);
|
||||
|
||||
// Don't render if no date
|
||||
if (!startDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: 1.5,
|
||||
cursor: "pointer",
|
||||
padding: 1,
|
||||
borderRadius: 1,
|
||||
"&:hover": {
|
||||
backgroundColor: "action.hover",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AccessTimeIcon sx={{ color: "text.secondary", mt: 0.5 }} />
|
||||
<Box flex={1}>
|
||||
<Box display="flex" gap={2} alignItems="center" mb={0.5}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: alpha(theme.palette.grey[900], 0.9), fontWeight: 500 }}
|
||||
>
|
||||
{dateText}
|
||||
</Typography>
|
||||
{timeText && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: alpha(theme.palette.grey[900], 0.9), fontWeight: 500 }}
|
||||
>
|
||||
{timeText}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Box display="flex" gap={2} alignItems="center">
|
||||
<Typography variant="caption" sx={{ color: "#444746" }}>
|
||||
{timezoneText}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: "#444746" }}>
|
||||
{repeatText}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user