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: select team members to receive emails #13957

Merged
merged 35 commits into from Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
45ad054
Add UI to select members to receive emails in routing form
asadath1395 Mar 2, 2024
e49a9b1
Add backend changes to support frontend to select team members to get…
asadath1395 Mar 3, 2024
93c8f82
Merge with upstream
asadath1395 Mar 3, 2024
23880dd
Fix type error
asadath1395 Mar 4, 2024
6b39816
Fix all the members of all the teams the user is part of shown instea…
asadath1395 Mar 4, 2024
9b852c8
Fix type error
asadath1395 Mar 5, 2024
837dbba
Merge branch 'main' into add-team-members-receive-emails
asadath1395 Mar 5, 2024
c841056
Merge branch 'main' into add-team-members-receive-emails
asadath1395 Mar 5, 2024
2859d7a
Merge with upstream
asadath1395 Mar 5, 2024
87c2167
Merge branch 'add-team-members-receive-emails' of https://github.com/…
asadath1395 Mar 5, 2024
46d7da0
Merge branch 'main' into add-team-members-receive-emails
asadath1395 Mar 5, 2024
8f9043d
Merge branch 'main' into add-team-members-receive-emails
asadath1395 Mar 6, 2024
ae825f4
Merge branch 'main' into add-team-members-receive-emails
asadath1395 Mar 6, 2024
10a1d85
Merge branch 'main' into add-team-members-receive-emails
asadath1395 Mar 7, 2024
93f2dc4
Merge branch 'main' into add-team-members-receive-emails
asadath1395 Mar 9, 2024
19bbbd0
Merge branch 'main' of https://github.com/calcom/cal.com into add-tea…
asadath1395 Mar 12, 2024
b68a4ab
Refactor components to a shared folder
asadath1395 Mar 12, 2024
bb5028a
Merge branch 'add-team-members-receive-emails' of https://github.com/…
asadath1395 Mar 12, 2024
b9677cd
Merge branch 'main' into add-team-members-receive-emails
asadath1395 Mar 12, 2024
1dbf7d1
Merge branch 'main' into add-team-members-receive-emails
asadath1395 Mar 12, 2024
478a481
Merge branch 'main' into add-team-members-receive-emails
asadath1395 Mar 13, 2024
a6d75dc
fix type error
CarinaWolli Mar 14, 2024
e8b25e9
Merge with upstream and fix merge conflicts
asadath1395 Mar 15, 2024
8b7170d
Merge branch 'add-team-members-receive-emails' of https://github.com/…
asadath1395 Mar 15, 2024
6b7a554
Merge branch 'main' into add-team-members-receive-emails
joeauyeung Mar 18, 2024
3484e6e
Merge branch 'main' of https://github.com/calcom/cal.com into add-tea…
asadath1395 Mar 19, 2024
8493678
Fix review comment: only include emails
asadath1395 Mar 19, 2024
81e4d1b
Remove listMembers trpc call in frontend and get the data in formQuer…
asadath1395 Mar 20, 2024
319cf6d
Address review comments
asadath1395 Mar 20, 2024
2352550
Merge branch 'add-team-members-receive-emails' of https://github.com/…
asadath1395 Mar 20, 2024
47cc320
Merge branch 'main' into add-team-members-receive-emails
asadath1395 Mar 20, 2024
4d2cfb0
Remove unrelated changes
asadath1395 Mar 20, 2024
79d9346
Merge branch 'add-team-members-receive-emails' of https://github.com/…
asadath1395 Mar 20, 2024
5206fd8
Update SingleForm.tsx
CarinaWolli Mar 21, 2024
7e6c566
Merge branch 'main' into add-team-members-receive-emails
CarinaWolli Mar 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
181 changes: 5 additions & 176 deletions apps/web/components/eventtype/EventTeamTab.tsx
Expand Up @@ -6,36 +6,16 @@
import { Controller, useFormContext, useWatch } from "react-hook-form";
import type { Options } from "react-select";

import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import AddMembersWithSwitch, {
mapUserToValue,
} from "@calcom/features/eventtypes/components/AddMembersWithSwitch";
import AssignAllTeamMembers from "@calcom/features/eventtypes/components/AssignAllTeamMembers";
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import type { FormValues, TeamMember } from "@calcom/features/eventtypes/lib/types";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import { Label, Select, SettingsToggle } from "@calcom/ui";

interface IUserToValue {
id: number | null;
name: string | null;
username: string | null;
avatar: string;
email: string;
}

type TeamMember = {
value: string;
label: string;
avatar: string;
email: string;
};

const mapUserToValue = ({ id, name, username, avatar, email }: IUserToValue, pendingString: string) => ({
value: `${id || ""}`,
label: `${name || email || ""}${!username ? ` (${pendingString})` : ""}`,
avatar,
email,
});

export const mapMemberToChildrenOption = (
member: EventTypeSetupProps["teamMembers"][number],
slug: string,
Expand All @@ -59,16 +39,6 @@
};
};

const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
if (a.label < b.label) {
return -1;
}
if (a.label > b.label) {
return 1;
}
return 0;
};

const ChildrenEventTypesList = ({
options = [],
value,
Expand Down Expand Up @@ -105,94 +75,6 @@
);
};

const AssignAllTeamMembers = ({
assignAllTeamMembers,
setAssignAllTeamMembers,
onActive,
onInactive,
}: {
assignAllTeamMembers: boolean;
setAssignAllTeamMembers: Dispatch<SetStateAction<boolean>>;
onActive: () => void;
onInactive?: () => void;
}) => {
const { t } = useLocale();
const { setValue } = useFormContext<FormValues>();

return (
<Controller<FormValues>
name="assignAllTeamMembers"
render={() => (
<SettingsToggle
title={t("automatically_add_all_team_members")}
labelClassName="mt-0.5 font-normal"
checked={assignAllTeamMembers}
onCheckedChange={(active) => {
setValue("assignAllTeamMembers", active, { shouldDirty: true });
setAssignAllTeamMembers(active);
if (active) {
onActive();
} else if (!!onInactive) {
onInactive();
}
}}
/>
)}
/>
);
};

const CheckedHostField = ({
labelText,
placeholder,
options = [],
isFixed,
value,
onChange,
helperText,
...rest
}: {
labelText?: string;
placeholder: string;
isFixed: boolean;
value: Host[];
onChange?: (options: Host[]) => void;
options?: Options<CheckedSelectOption>;
helperText?: React.ReactNode | string;
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
return (
<div className="flex flex-col rounded-md">
<div>
{labelText ? <Label>{labelText}</Label> : <></>}
<CheckedTeamSelect
isOptionDisabled={(option) => !!value.find((host) => host.userId.toString() === option.value)}
onChange={(options) => {
onChange &&
onChange(
options.map((option) => ({
isFixed,
userId: parseInt(option.value, 10),
priority: option.priority ?? 2,
}))
);
}}
value={(value || [])
.filter(({ isFixed: _isFixed }) => isFixed === _isFixed)
.map((host) => {
const option = options.find((member) => member.value === host.userId.toString());
return option ? { ...option, priority: host.priority ?? 2, isFixed } : options[0];
})
.filter(Boolean)}
controlShouldRenderValue={false}
options={options}
placeholder={placeholder}
{...rest}
/>
</div>
</div>
);
};

const FixedHostHelper = (
<Trans i18nKey="fixed_host_helper">
Add anyone who needs to attend the event.
Expand Down Expand Up @@ -304,59 +186,6 @@
);
};

const AddMembersWithSwitch = ({
teamMembers,
value,
onChange,
assignAllTeamMembers,
setAssignAllTeamMembers,
automaticAddAllEnabled,
onActive,
isFixed,
}: {
value: Host[];
onChange: (hosts: Host[]) => void;
teamMembers: TeamMember[];
assignAllTeamMembers: boolean;
setAssignAllTeamMembers: Dispatch<SetStateAction<boolean>>;
automaticAddAllEnabled: boolean;
onActive: () => void;
isFixed: boolean;
}) => {
const { t } = useLocale();
const { setValue } = useFormContext<FormValues>();

return (
<div className="rounded-md ">
<div className="flex flex-col rounded-md px-6 pb-2 pt-6">
{automaticAddAllEnabled ? (
<div className="mb-2">
<AssignAllTeamMembers
assignAllTeamMembers={assignAllTeamMembers}
setAssignAllTeamMembers={setAssignAllTeamMembers}
onActive={onActive}
onInactive={() => setValue("hosts", [], { shouldDirty: true })}
/>
</div>
) : (
<></>
)}
{!assignAllTeamMembers || !automaticAddAllEnabled ? (
<CheckedHostField
value={value}
onChange={onChange}
isFixed={isFixed}
options={teamMembers.sort(sortByLabel)}
placeholder={t("add_attendees")}
/>
) : (
<></>
)}
</div>
</div>
);
};

const RoundRobinHosts = ({
teamMembers,
value,
Expand Down Expand Up @@ -450,7 +279,7 @@
assignAllTeamMembers: boolean;
setAssignAllTeamMembers: Dispatch<SetStateAction<boolean>>;
}) => {
const { t } = useLocale();

Check warning on line 282 in apps/web/components/eventtype/EventTeamTab.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

apps/web/components/eventtype/EventTeamTab.tsx#L282

[@typescript-eslint/no-unused-vars] 't' is assigned a value but never used. Allowed unused vars must match /^_/u.
const {
control,
setValue,
Expand Down
17 changes: 9 additions & 8 deletions apps/web/public/static/locales/en/common.json
Expand Up @@ -1085,7 +1085,7 @@
"make_team_private": "Make team private",
"make_team_private_description": "Your team members won't be able to see other team members when this is turned on.",
"you_cannot_see_team_members": "You cannot see all the team members of a private team.",
"you_cannot_see_teams_of_org":"You cannot see teams of a private organization.",
"you_cannot_see_teams_of_org": "You cannot see teams of a private organization.",
"allow_booker_to_select_duration": "Allow booker to select duration",
"impersonate_user_tip": "All uses of this feature is audited.",
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
Expand Down Expand Up @@ -2313,12 +2313,13 @@
"confirm_email": "Confirm your email",
"confirm_email_description": "We sent an email to <strong>{{email}}</strong>. Click the link in the email to verify this address.",
"send_event_details_to": "Send event details to",
"lock_event_types_modal_header":"What should we do with your member's existing event types?",
"org_delete_event_types_org_admin":"All of your members individual event types (except managed ones) will be permanently deleted. They will not be able to create new ones",
"org_hide_event_types_org_admin":"Your members individual event types will be hidden (except managed ones) from profiles but the links will still be active. They will not be able to create new ones. ",
"hide_org_eventtypes":"Hide individual event types",
"delete_org_eventtypes":"Delete individual event types",
"lock_org_users_eventtypes":"Lock individual event type creation",
"lock_org_users_eventtypes_description":"Prevent members from creating their own event types.",
"select_members": "Select members",
"lock_event_types_modal_header": "What should we do with your member's existing event types?",
"org_delete_event_types_org_admin": "All of your members individual event types (except managed ones) will be permanently deleted. They will not be able to create new ones",
"org_hide_event_types_org_admin": "Your members individual event types will be hidden (except managed ones) from profiles but the links will still be active. They will not be able to create new ones. ",
"hide_org_eventtypes": "Hide individual event types",
"delete_org_eventtypes": "Delete individual event types",
"lock_org_users_eventtypes": "Lock individual event type creation",
"lock_org_users_eventtypes_description": "Prevent members from creating their own event types.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
94 changes: 80 additions & 14 deletions packages/app-store/routing-forms/components/SingleForm.tsx
Expand Up @@ -5,6 +5,8 @@
import { Controller, useFormContext } from "react-hook-form";

import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import AddMembersWithSwitch from "@calcom/features/eventtypes/components/AddMembersWithSwitch";
import type { Host } from "@calcom/features/eventtypes/lib/types";
import { ShellMain } from "@calcom/features/shell/Shell";
import useApp from "@calcom/lib/hooks/useApp";
import { useLocale } from "@calcom/lib/hooks/useLocale";
Expand Down Expand Up @@ -243,11 +245,16 @@
function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleFormComponentProps) {
const utils = trpc.useContext();
const { t } = useLocale();
const { data: teamMembers } = form.teamId
? trpc.viewer.teams.listMembers.useQuery({ teamIds: [form.teamId] })
: { data: [] };
Copy link
Contributor

Choose a reason for hiding this comment

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

If we are querying the team members everytime the form has a teamId can we move this to server side and include it in the formQuery.handler?

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


const [isTestPreviewOpen, setIsTestPreviewOpen] = useState(false);
const [response, setResponse] = useState<Response>({});
const [decidedAction, setDecidedAction] = useState<Route["action"] | null>(null);
const [skipFirstUpdate, setSkipFirstUpdate] = useState(true);
const [selectedMembers, setSelectedMembers] = useState<Host[]>([]);
const [assignAllTeamMembers, setAssignAllTeamMembers] = useState(false);
const [eventTypeUrl, setEventTypeUrl] = useState("");

function testRouting() {
Expand Down Expand Up @@ -293,6 +300,26 @@
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [form]);

useEffect(() => {
if (form.teamId && form.settings?.sendUpdatesTo?.length && teamMembers?.length) {
let sendToAll = true;
teamMembers.forEach((member) => {
if (!form.settings?.sendUpdatesTo?.includes(member.id)) {
sendToAll = false;
return;
}
});
setAssignAllTeamMembers(sendToAll);
setSelectedMembers(
form.settings.sendUpdatesTo.map((userId) => ({
isFixed: true,
userId: userId,
priority: 1,
}))
);
}
}, [form.teamId, form.settings?.sendUpdatesTo?.length, teamMembers?.length]);

Check warning on line 321 in packages/app-store/routing-forms/components/SingleForm.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app-store/routing-forms/components/SingleForm.tsx#L321

[react-hooks/exhaustive-deps] React Hook useEffect has missing dependencies: 'form.settings.sendUpdatesTo' and 'teamMembers'. Either include them or remove the dependency array. If 'setSelectedMembers' needs the current value of 'form.settings.sendUpdatesTo', you can also switch to useReducer instead of useState and read 'form.settings.sendUpdatesTo' in the reducer.
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we instead add to the form a sendToAll and a sendUpdatesTo field?

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


const mutation = trpc.viewer.appRoutingForms.formMutation.useMutation({
onSuccess() {
showToast(t("form_updated_successfully"), "success");
Expand Down Expand Up @@ -356,20 +383,59 @@
/>

<div className="mt-6">
<Controller
name="settings.emailOwnerOnSubmission"
control={hookForm.control}
render={({ field: { value, onChange } }) => {
return (
<SettingsToggle
title={t("routing_forms_send_email_owner")}
description={t("routing_forms_send_email_owner_description")}
checked={value}
onCheckedChange={(val) => onChange(val)}
/>
);
}}
/>
{form.teamId ? (
<AddMembersWithSwitch
teamMembers={(teamMembers || []).map((member) => ({
value: member.id.toString(),
label: member.name || "",
avatar: member.avatarUrl || "",
email: member.email,
isFixed: true,
}))}
value={selectedMembers}
onChange={(value) => {
setSelectedMembers(value);
hookForm.setValue(
"settings.sendUpdatesTo",
value.map((teamMember) => teamMember.userId),
{ shouldDirty: true }
);
hookForm.setValue("settings.emailOwnerOnSubmission", false, { shouldDirty: true });
}}
assignAllTeamMembers={assignAllTeamMembers}
setAssignAllTeamMembers={setAssignAllTeamMembers}
automaticAddAllEnabled={true}
isFixed={true}
onActive={() => {
hookForm.setValue(
"settings.sendUpdatesTo",
(teamMembers || []).map((teamMember) => teamMember.id),
{ shouldDirty: true }
);
hookForm.setValue("settings.emailOwnerOnSubmission", false, { shouldDirty: true });
}}
placeholder={t("select_members")}
containerClassName="!px-0 !pb-0 !pt-0"
/>
) : (
<Controller
name="settings.emailOwnerOnSubmission"
control={hookForm.control}
render={({ field: { value, onChange } }) => {
return (
<SettingsToggle
title={t("routing_forms_send_email_owner")}
description={t("routing_forms_send_email_owner_description")}
checked={value}
onCheckedChange={(val) => {
onChange(val);
hookForm.unregister("settings.sendUpdatesTo");
}}
/>
);
}}
/>
)}
</div>

{form.routers.length ? (
Expand Down