Files
calendar/packages/features/settings/CreateOrEditOutOfOfficeModal.tsx
T
ad5d645347 feat: Implement editable OOO events feature (#15932)
* feat: Implement edit ooo feature

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* refactor: Refactor button text and gap between buttons

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* fix: Fix entry create and edit edge cases

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* chore: Change "edit ooo event" modal title

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* chore: Add tooltips to entry eidt and delete buttons

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* feat: Reset form on form close event

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* refactor: Refactor reset-form function

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* chore: Rename create or edit OOO as suggested

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* refactor: Refactor component and function names

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* feat: Improve ooo events email notifications

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* fix: Fix email templates

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* feat: Add e2e test for ooo event edit functionality

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* refactor: Create separate test for edit ooo event

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* fix: Remove extra user from redirect user configuration test

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* chore: code refactor

* remove log

* chore: Update ooo-input-schema uuid data type

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* fix: Address feedbacks

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* chore: code splitting and refactor

---------

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-08-29 17:28:35 +00:00

244 lines
8.1 KiB
TypeScript

import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import dayjs from "@calcom/dayjs";
import { useHasTeamPlan } from "@calcom/lib/hooks/useHasPaidPlan";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import {
Button,
DateRangePicker,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
Select,
showToast,
Switch,
TextArea,
UpgradeTeamsBadge,
} from "@calcom/ui";
import type { BookingRedirectForm } from "@calcom/web/pages/settings/my-account/out-of-office";
export const CreateOrEditOutOfOfficeEntryModal = ({
openModal,
closeModal,
currentlyEditingOutOfOfficeEntry,
}: {
openModal: boolean;
closeModal: () => void;
currentlyEditingOutOfOfficeEntry: BookingRedirectForm | null;
}) => {
const { t } = useLocale();
const utils = trpc.useUtils();
const { data: listMembers } = trpc.viewer.teams.listMembers.useQuery({});
const me = useMeQuery();
const memberListOptions: {
value: number;
label: string;
}[] =
listMembers
?.filter((member) => me?.data?.id !== member.id)
.map((member) => ({
value: member.id,
label: member.name || "",
})) || [];
type Option = { value: number; label: string };
const { data: outOfOfficeReasonList } = trpc.viewer.outOfOfficeReasonList.useQuery();
const reasonList = (outOfOfficeReasonList || []).map((reason) => ({
label: `${reason.emoji} ${reason.userId === null ? t(reason.reason) : reason.reason}`,
value: reason.id,
}));
const [profileRedirect, setProfileRedirect] = useState(!!currentlyEditingOutOfOfficeEntry?.toTeamUserId);
const { hasTeamPlan } = useHasTeamPlan();
const { handleSubmit, setValue, control, register } = useForm<BookingRedirectForm>({
defaultValues: currentlyEditingOutOfOfficeEntry
? currentlyEditingOutOfOfficeEntry
: {
dateRange: {
startDate: dayjs().utc().startOf("d").toDate(),
endDate: dayjs().utc().add(1, "d").endOf("d").toDate(),
},
offset: dayjs().utcOffset(),
toTeamUserId: null,
reasonId: 1,
},
});
const createOrEditOutOfOfficeEntry = trpc.viewer.outOfOfficeCreateOrUpdate.useMutation({
onSuccess: () => {
showToast(
currentlyEditingOutOfOfficeEntry
? t("success_edited_entry_out_of_office")
: t("success_entry_created"),
"success"
);
utils.viewer.outOfOfficeEntriesList.invalidate();
closeModal();
},
onError: (error) => {
showToast(t(error.message), "error");
},
});
return (
<Dialog open={openModal}>
<DialogContent
onOpenAutoFocus={(event) => {
event.preventDefault();
}}>
<form
id="create-or-edit-ooo-form"
className="h-full"
onSubmit={handleSubmit((data) => {
createOrEditOutOfOfficeEntry.mutate(data);
})}>
<div className="px-1">
<DialogHeader
title={
currentlyEditingOutOfOfficeEntry ? t("edit_an_out_of_office") : t("create_an_out_of_office")
}
/>
<div>
<p className="text-emphasis mb-1 block text-sm font-medium capitalize">{t("dates")}</p>
<div>
<Controller
name="dateRange"
control={control}
render={({ field: { onChange, value } }) => (
<DateRangePicker
dates={{ startDate: value.startDate, endDate: value.endDate }}
onDatesChange={(values) => {
onChange(values);
}}
/>
)}
/>
</div>
</div>
{/* Reason Select */}
<div className="mt-4 w-full">
<div className="">
<p className="text-emphasis block text-sm font-medium">{t("reason")}</p>
<Controller
control={control}
name="reasonId"
render={({ field: { onChange, value } }) => (
<Select<Option>
className="mb-0 mt-1 text-white"
name="reason"
data-testid="reason_select"
value={reasonList.find((reason) => reason.value === value)}
placeholder={t("ooo_select_reason")}
options={reasonList}
onChange={(selectedOption) => {
if (selectedOption?.value) {
onChange(selectedOption.value);
}
}}
/>
)}
/>
</div>
</div>
{/* Notes input */}
<div className="mt-4">
<p className="text-emphasis block text-sm font-medium">{t("notes")}</p>
<TextArea
data-testid="notes_input"
className="border-subtle mt-1 h-10 w-full rounded-lg border px-2"
placeholder={t("additional_notes")}
{...register("notes")}
onChange={(e) => {
setValue("notes", e?.target.value);
}}
/>
</div>
<div className="bg-muted my-4 rounded-xl p-5">
<div className="flex flex-row">
<Switch
disabled={!hasTeamPlan}
data-testid="profile-redirect-switch"
checked={profileRedirect}
id="profile-redirect-switch"
onCheckedChange={(state) => {
setProfileRedirect(state);
if (!state) {
setValue("toTeamUserId", null);
}
}}
label={hasTeamPlan ? t("redirect_team_enabled") : t("redirect_team_disabled")}
/>
{!hasTeamPlan && (
<div className="mx-2" data-testid="upgrade-team-badge">
<UpgradeTeamsBadge />
</div>
)}
</div>
{profileRedirect && (
<div className="mt-4">
<div className="h-16">
<p className="text-emphasis block text-sm font-medium">{t("team_member")}</p>
<Controller
control={control}
name="toTeamUserId"
render={({ field: { onChange, value } }) => (
<Select<Option>
name="toTeamUsername"
data-testid="team_username_select"
value={memberListOptions.find((member) => member.value === value)}
placeholder={t("select_team_member")}
isSearchable
options={memberListOptions}
onChange={(selectedOption) => {
if (selectedOption?.value) {
onChange(selectedOption.value);
}
}}
/>
)}
/>
</div>
</div>
)}
</div>
</div>
</form>
<DialogFooter showDivider noSticky>
<div className="flex">
<Button
color="minimal"
type="button"
onClick={() => {
closeModal();
}}
className="mr-1">
{t("cancel")}
</Button>
<Button
form="create-or-edit-ooo-form"
color="primary"
type="submit"
disabled={createOrEditOutOfOfficeEntry.isPending}
data-testid="create-or-edit-entry-ooo-redirect">
{currentlyEditingOutOfOfficeEntry ? t("save") : t("create")}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
};