Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement editable OOO events feature #15932

Merged
merged 29 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2b39a67
feat: Implement edit ooo feature
Souptik2001 Jul 26, 2024
b109791
Merge branch 'main' into feature-15828-edit-ooo-events
Souptik2001 Jul 26, 2024
07a0e74
refactor: Refactor button text and gap between buttons
Souptik2001 Jul 26, 2024
b737ad1
fix: Fix entry create and edit edge cases
Souptik2001 Jul 27, 2024
410bdc0
chore: Change "edit ooo event" modal title
Souptik2001 Jul 27, 2024
9625ab3
chore: Add tooltips to entry eidt and delete buttons
Souptik2001 Jul 27, 2024
e8f0dc3
feat: Reset form on form close event
Souptik2001 Jul 27, 2024
cbbbad0
refactor: Refactor reset-form function
Souptik2001 Jul 27, 2024
64127f8
chore: Rename create or edit OOO as suggested
Souptik2001 Jul 28, 2024
825bb46
refactor: Refactor component and function names
Souptik2001 Jul 28, 2024
1cd1df1
feat: Improve ooo events email notifications
Souptik2001 Jul 28, 2024
0862e92
Merge branch 'main' into feature-15828-edit-ooo-events
Souptik2001 Jul 29, 2024
909498e
fix: Fix email templates
Souptik2001 Jul 29, 2024
27d2100
feat: Add e2e test for ooo event edit functionality
Souptik2001 Jul 29, 2024
f0c7a7f
refactor: Create separate test for edit ooo event
Souptik2001 Jul 29, 2024
a3bf1e9
fix: Remove extra user from redirect user configuration test
Souptik2001 Jul 29, 2024
d47952f
Merge branch 'main' into feature-15828-edit-ooo-events
Amit91848 Aug 6, 2024
1a68ebf
chore: code refactor
Amit91848 Aug 7, 2024
b385146
remove log
Amit91848 Aug 7, 2024
298965d
Merge branch 'main' into feature-15828-edit-ooo-events
Amit91848 Aug 7, 2024
53a41a3
chore: Update ooo-input-schema uuid data type
Souptik2001 Aug 7, 2024
95f41ce
Merge branch 'main' into feature-15828-edit-ooo-events
Souptik2001 Aug 7, 2024
92777c7
Merge branch 'main' into feature-15828-edit-ooo-events
Souptik2001 Aug 17, 2024
5c8c10c
fix: Address feedbacks
Souptik2001 Aug 17, 2024
57b7b59
Merge branch 'main' into feature-15828-edit-ooo-events
Amit91848 Aug 29, 2024
acea1ab
chore: code splitting and refactor
Amit91848 Aug 29, 2024
470e01c
Merge branch 'main' into feature-15828-edit-ooo-events
anikdhabal Aug 29, 2024
ea2f347
fix: e2e
Amit91848 Aug 29, 2024
c9f6ec3
Merge branch 'main' into feature-15828-edit-ooo-events
Amit91848 Aug 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 153 additions & 41 deletions apps/web/pages/settings/my-account/out-of-office/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Trans } from "next-i18next";
import { useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Controller, useForm, useFormState } from "react-hook-form";

import dayjs from "@calcom/dayjs";
Expand Down Expand Up @@ -39,18 +39,58 @@ export type BookingRedirectForm = {
toTeamUserId: number | null;
reasonId: number;
notes?: string;
uuid?: string;
};

export type outOfOfficeEntryData = {
uuid: string;
selectedReason: number | null;
profileRedirect: boolean;
selectedMember: number | null;
notes: string | null;
start: Date;
end: Date;
};

const CreateOutOfOfficeEntryModal = ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be CreateOrUpdateOutOfOfficeEntryModal. In fact, everything needs to be changed from create to createOrUpdate. Please go through the code thoroughly and make the necessary changes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it.. will do it..

openModal,
closeModal,
currentlyEditingOutOfOfficeEntry,
anikdhabal marked this conversation as resolved.
Show resolved Hide resolved
}: {
openModal: boolean;
closeModal: () => void;
currentlyEditingOutOfOfficeEntry: outOfOfficeEntryData | null;
}) => {
const { t } = useLocale();
const utils = trpc.useUtils();

const { data: listMembers } = trpc.viewer.teams.listMembers.useQuery({});
const me = useMeQuery();
const memberListOptions: {
value: number | null;
label: string;
}[] = useMemo(() => {
return (
listMembers
?.filter((member) => me?.data?.id !== member.id)
.map((member) => ({
value: member.id || null,
label: member.name || "",
})) || []
);
}, [listMembers, me?.data?.id]);

const { data: outOfOfficeReasonList } = trpc.viewer.outOfOfficeReasonList.useQuery();

const reasonList = useMemo(() => {
return [
...(outOfOfficeReasonList || []).map((reason) => ({
label: `${reason.emoji} ${reason.userId === null ? t(reason.reason) : reason.reason}`,
value: reason.id,
})),
];
}, [outOfOfficeReasonList, t]);
anikdhabal marked this conversation as resolved.
Show resolved Hide resolved

const [selectedReason, setSelectedReason] = useState<{ label: string; value: number } | null>(null);
const [profileRedirect, setProfileRedirect] = useState(false);
const [selectedMember, setSelectedMember] = useState<{ label: string; value: number | null } | null>(null);
anikdhabal marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -61,18 +101,6 @@ const CreateOutOfOfficeEntryModal = ({
});

const { hasTeamPlan } = useHasTeamPlan();
const { data: listMembers } = trpc.viewer.teams.listMembers.useQuery({});
const me = useMeQuery();
const memberListOptions: {
value: number | null;
label: string;
}[] =
listMembers
?.filter((member) => me?.data?.id !== member.id)
.map((member) => ({
value: member.id || null,
label: member.name || "",
})) || [];

const { handleSubmit, setValue, control, register } = useForm<BookingRedirectForm>({
defaultValues: {
Expand All @@ -88,8 +116,16 @@ const CreateOutOfOfficeEntryModal = ({

const createOutOfOfficeEntry = trpc.viewer.outOfOfficeCreate.useMutation({
onSuccess: () => {
showToast(t("success_entry_created"), "success");
if (currentlyEditingOutOfOfficeEntry) {
showToast(t("success_edited_entry_out_of_office"), "success");
} else {
showToast(t("success_entry_created"), "success");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could do it in one line:-

showToast(currentlyEditingOutOfOfficeEntry ? t("success_edited_entry_out_of_office") : t("success_entry_created"), "success");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Done!

utils.viewer.outOfOfficeEntriesList.invalidate();
setValue("toTeamUserId", null);
setValue("notes", "");
setSelectedReason(null);
setSelectedMember(null);
setProfileRedirect(false);
closeModal();
},
Expand All @@ -98,14 +134,50 @@ const CreateOutOfOfficeEntryModal = ({
},
});

const { data: outOfOfficeReasonList } = trpc.viewer.outOfOfficeReasonList.useQuery();
useEffect(() => {
if (currentlyEditingOutOfOfficeEntry === null) {
setValue("toTeamUserId", null);
setValue("notes", "");
setValue("dateRange", dateRange);
setValue("reasonId", 1);
setSelectedReason(null);
setSelectedMember(null);
setProfileRedirect(false);
return;
}

const selectedReason =
reasonList.find((reason) => {
return reason.value === currentlyEditingOutOfOfficeEntry.selectedReason;
}) ?? null;
if (selectedReason && selectedReason.value) {
setValue("reasonId", selectedReason.value);
setSelectedReason(selectedReason);
}

const selectedMember =
memberListOptions.find((member) => {
return member.value === currentlyEditingOutOfOfficeEntry.selectedMember;
}) ?? null;

setProfileRedirect(currentlyEditingOutOfOfficeEntry.profileRedirect);

if (selectedMember && selectedMember?.value) {
setValue("toTeamUserId", selectedMember.value);
setSelectedMember(selectedMember);
}

if (currentlyEditingOutOfOfficeEntry.notes) {
setValue("notes", currentlyEditingOutOfOfficeEntry.notes);
}

const reasonList = [
...(outOfOfficeReasonList || []).map((reason) => ({
label: `${reason.emoji} ${reason.userId === null ? t(reason.reason) : reason.reason}`,
value: reason.id,
})),
];
setValue("dateRange", {
startDate: dayjs(currentlyEditingOutOfOfficeEntry.start).startOf("d").toDate(),
endDate: dayjs(currentlyEditingOutOfOfficeEntry.end).subtract(1, "d").endOf("d").toDate(),
});

setValue("uuid", currentlyEditingOutOfOfficeEntry.uuid);
}, [currentlyEditingOutOfOfficeEntry, setValue, reasonList, dateRange, memberListOptions]);

return (
<Dialog open={openModal}>
Expand All @@ -118,13 +190,13 @@ const CreateOutOfOfficeEntryModal = ({
className="h-full"
onSubmit={handleSubmit((data) => {
createOutOfOfficeEntry.mutate(data);
setValue("toTeamUserId", null);
setValue("notes", "");
setSelectedReason(null);
setSelectedMember(null);
})}>
<div className="px-1">
<DialogHeader title={t("create_an_out_of_office")} />
<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>
Expand Down Expand Up @@ -234,7 +306,7 @@ const CreateOutOfOfficeEntryModal = ({
type="submit"
disabled={createOutOfOfficeEntry.isPending}
data-testid="create-entry-ooo-redirect">
{t("create")}
{currentlyEditingOutOfOfficeEntry ? t("edit") : t("create")}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of "edit" it should be "save"

</Button>
</div>
</DialogFooter>
Expand All @@ -243,7 +315,11 @@ const CreateOutOfOfficeEntryModal = ({
);
};

const OutOfOfficeEntriesList = () => {
const OutOfOfficeEntriesList = ({
editOutOfOfficeEntry,
}: {
editOutOfOfficeEntry: (entry: outOfOfficeEntryData) => void;
}) => {
const { t } = useLocale();
const utils = trpc.useUtils();
const { data, isPending } = trpc.viewer.outOfOfficeEntriesList.useQuery();
Expand Down Expand Up @@ -324,17 +400,38 @@ const OutOfOfficeEntriesList = () => {
</div>
</div>

<Button
className="self-center rounded-lg border"
type="button"
color="minimal"
variant="icon"
disabled={deleteOutOfOfficeEntryMutation.isPending}
StartIcon="trash-2"
onClick={() => {
deleteOutOfOfficeEntryMutation.mutate({ outOfOfficeUid: item.uuid });
}}
/>
<div className="flex flex-row items-center">
Copy link
Contributor

@anikdhabal anikdhabal Jul 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need space between these button

<Button
className="self-center rounded-lg border"
type="button"
color="minimal"
variant="icon"
StartIcon="pencil"
onClick={() => {
const outOfOfficeEntryData = {
uuid: item.uuid,
profileRedirect: item.toUserId ? true : false,
selectedReason: item.reason?.id ?? null,
selectedMember: item.toUserId,
notes: item.notes,
start: item.start,
end: item.end,
};
editOutOfOfficeEntry(outOfOfficeEntryData);
}}
/>
<Button
className="self-center rounded-lg border"
type="button"
color="minimal"
variant="icon"
disabled={deleteOutOfOfficeEntryMutation.isPending}
StartIcon="trash-2"
onClick={() => {
deleteOutOfOfficeEntryMutation.mutate({ outOfOfficeUid: item.uuid });
}}
/>
</div>
</TableCell>
</TableRow>
))}
Expand Down Expand Up @@ -375,6 +472,14 @@ const OutOfOfficePage = () => {
}, [openModalOnStart]);

const [openModal, setOpenModal] = useState(false);
const [currentlyEditingOutOfOfficeEntry, setCurrentlyEditingOutOfOfficeEntry] =
useState<outOfOfficeEntryData | null>(null);

const editOutOfOfficeEntry = (entry: outOfOfficeEntryData) => {
setCurrentlyEditingOutOfOfficeEntry(entry);
setOpenModal(true);
};

return (
<>
<Meta
Expand All @@ -391,8 +496,15 @@ const OutOfOfficePage = () => {
</Button>
}
/>
<CreateOutOfOfficeEntryModal openModal={openModal} closeModal={() => setOpenModal(false)} />
<OutOfOfficeEntriesList />
<CreateOutOfOfficeEntryModal
openModal={openModal}
closeModal={() => {
setOpenModal(false);
setCurrentlyEditingOutOfOfficeEntry(null);
}}
currentlyEditingOutOfOfficeEntry={currentlyEditingOutOfOfficeEntry}
/>
<OutOfOfficeEntriesList editOutOfOfficeEntry={editOutOfOfficeEntry} />
</>
);
};
Expand Down
2 changes: 2 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2333,6 +2333,7 @@
"redirect_team_disabled": "Provide a link to a team member when OOO (Team plan required)",
"out_of_office_unavailable_list": "Out of office unavailability list",
"success_deleted_entry_out_of_office": "Successfully deleted entry",
"success_edited_entry_out_of_office": "Successfully edited entry",
"temporarily_out_of_office": "Temporarily Out-Of-Office?",
"add_a_redirect": "Add a redirect",
"create_entry": "Create entry",
Expand Down Expand Up @@ -2454,6 +2455,7 @@
"ooo_create_entry_modal": "Go Out of Office",
"ooo_select_reason": "Select reason",
"create_an_out_of_office": "Go Out of Office",
"edit_an_out_of_office": "Edit this Out of Office Entry",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT:

Suggested change
"edit_an_out_of_office": "Edit this Out of Office Entry",
"edit_an_out_of_office": "Edit Out of Office Entry",

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

"submit_feedback": "Submit Feedback",
"host_no_show": "Your host didn't show up",
"no_show_description": "You can reschedule another meeting with them",
Expand Down
20 changes: 18 additions & 2 deletions packages/trpc/server/routers/loggedInViewer/outOfOffice.handler.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anikdhabal Just FYI - To avoid creation of another new mutation, I have made the outOfOfficeCreate only handle the edit functionality also, as it didn't pose much complexity and also looks clean.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked at all the code thoroughly; I will check it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! 👍

Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ export const outOfOfficeCreate = async ({ ctx, input }: TBookingRedirect) => {
where: {
AND: [
{ userId: ctx.user.id },
{
uuid: {
not: input.uuid,
},
},
{
OR: [
{
Expand Down Expand Up @@ -154,8 +159,11 @@ export const outOfOfficeCreate = async ({ ctx, input }: TBookingRedirect) => {
const startDateUtc = dayjs.utc(startDate).add(input.offset, "minute");
const endDateUtc = dayjs.utc(endDate).add(input.offset, "minute");

const createdRedirect = await prisma.outOfOfficeEntry.create({
data: {
const createdRedirect = await prisma.outOfOfficeEntry.upsert({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change it to createdOrUpdatedRedirect

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

where: {
uuid: input.uuid ?? "",
},
create: {
uuid: uuidv4(),
start: startDateUtc.startOf("day").toISOString(),
end: endDateUtc.endOf("day").toISOString(),
Expand All @@ -166,6 +174,14 @@ export const outOfOfficeCreate = async ({ ctx, input }: TBookingRedirect) => {
createdAt: new Date(),
updatedAt: new Date(),
},
update: {
start: startDateUtc.startOf("day").toISOString(),
end: endDateUtc.endOf("day").toISOString(),
notes: input.notes,
userId: ctx.user.id,
reasonId: input.reasonId,
toUserId: toUserId,
},
});

if (toUserId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { z } from "zod";

export const ZOutOfOfficeInputSchema = z.object({
uuid: z.string().optional(),
dateRange: z.object({
startDate: z.date(),
endDate: z.date(),
Expand Down
Loading