-
Notifications
You must be signed in to change notification settings - Fork 12k
feat: add optional guest members from team #22127
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -81,6 +81,12 @@ export default class ExchangeCalendarService implements Calendar { | |||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (event.optionalGuestTeamMembers) { | ||||||||||||||||||||||||||||||
| event.optionalGuestTeamMembers.forEach((member: { email: string }) => { | ||||||||||||||||||||||||||||||
| appointment.OptionalAttendees.Add(new Attendee(member.email)); | ||||||||||||||||||||||||||||||
jayantpranjal0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+84
to
+88
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add email validation to prevent API errors. Consider filtering out team members without email addresses before adding them as attendees to prevent potential Exchange API errors. if (event.optionalGuestTeamMembers) {
- event.optionalGuestTeamMembers.forEach((member: { email: string }) => {
+ event.optionalGuestTeamMembers.filter(member => !!member.email).forEach((member: { email: string }) => {
appointment.OptionalAttendees.Add(new Attendee(member.email));
});
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| await appointment.Save(SendInvitationsMode.SendToAllAndSaveCopy); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||
|
|
@@ -118,6 +124,12 @@ export default class ExchangeCalendarService implements Calendar { | |||||||||||||||||||||||||||||
| appointment.RequiredAttendees.Add(new Attendee(member.email)); | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| appointment.OptionalAttendees.Clear(); | ||||||||||||||||||||||||||||||
| if (event.optionalGuestTeamMembers) { | ||||||||||||||||||||||||||||||
| event.optionalGuestTeamMembers.forEach((member: { email: string }) => { | ||||||||||||||||||||||||||||||
| appointment.OptionalAttendees.Add(new Attendee(member.email)); | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+127
to
+132
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add email validation for consistency. Apply the same email filtering logic here as suggested for the createEvent method. appointment.OptionalAttendees.Clear();
if (event.optionalGuestTeamMembers) {
- event.optionalGuestTeamMembers.forEach((member: { email: string }) => {
+ event.optionalGuestTeamMembers.filter(member => !!member.email).forEach((member: { email: string }) => {
appointment.OptionalAttendees.Add(new Attendee(member.email));
});
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| appointment.Update( | ||||||||||||||||||||||||||||||
| ConflictResolutionMode.AlwaysOverwrite, | ||||||||||||||||||||||||||||||
| SendInvitationsOrCancellationsMode.SendToAllAndSaveCopy | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -467,7 +467,7 @@ export default class ZohoCalendarService implements Calendar { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| end: dayjs(event.endTime).format("YYYYMMDDTHHmmssZZ"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timezone: event.organizer.timeZone, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| attendees: event.attendees.map((attendee) => ({ email: attendee.email })), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| attendees: this.getAttendees(event), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isprivate: event.seatsShowAttendees, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reminders: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -480,4 +480,18 @@ export default class ZohoCalendarService implements Calendar { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return zohoEvent; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private getAttendees = (event: CalendarEvent) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const attendees = event.attendees.map((attendee) => ({ email: attendee.email })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (event.optionalGuestTeamMembers) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| attendees.push( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...event.optionalGuestTeamMembers.map((member) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| email: member.email, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 2 is optional guest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| attendance: 2, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return attendees; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+484
to
+496
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add email validation for robustness. The refactoring into a separate method is excellent for maintainability. Consider adding email validation to prevent potential API errors when team members lack email addresses. private getAttendees = (event: CalendarEvent) => {
const attendees = event.attendees.map((attendee) => ({ email: attendee.email }));
if (event.optionalGuestTeamMembers) {
attendees.push(
- ...event.optionalGuestTeamMembers.map((member) => ({
+ ...event.optionalGuestTeamMembers.filter(member => !!member.email).map((member) => ({
email: member.email,
// 2 is optional guest
attendance: 2,
}))
);
}
return attendees;
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| import { useState, useEffect } from "react"; | ||
| import { Controller, useFormContext } from "react-hook-form"; | ||
|
|
||
| import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect"; | ||
| import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect"; | ||
| import { useLocale } from "@calcom/lib/hooks/useLocale"; | ||
| import classNames from "@calcom/ui/classNames"; | ||
| import { SettingsToggle } from "@calcom/ui/components/form"; | ||
|
|
||
| import type { EventTypeSetup, FormValues } from "../../../lib/types"; | ||
|
|
||
| type GuestTeamMemberControllerProps = { | ||
| team: EventTypeSetup["team"]; | ||
| eventType: EventTypeSetup; | ||
| }; | ||
|
|
||
| function GuestTeamMemberController({ team, eventType }: GuestTeamMemberControllerProps) { | ||
| const { t } = useLocale(); | ||
|
|
||
| const formMethods = useFormContext<FormValues>(); | ||
|
|
||
| const [isGuestTeamMembersEnabled, setIsGuestTeamMembersEnabled] = useState( | ||
jayantpranjal0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| eventType.optionalGuestTeamMembers.length > 0 | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
| setIsGuestTeamMembersEnabled(eventType.optionalGuestTeamMembers.length > 0); | ||
| }, [eventType.optionalGuestTeamMembers]); | ||
|
|
||
| const addedGuestTeamMembers = formMethods.watch("optionalGuestTeamMembers", []); | ||
|
|
||
| if (!team) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <div className="block w-full items-start sm:flex"> | ||
| <Controller | ||
| name="optionalGuestTeamMembers" | ||
| control={formMethods.control} | ||
| render={({ field: { onChange } }) => ( | ||
| <div className="w-full"> | ||
| <SettingsToggle | ||
| title={t("optional_guest_team_members")} | ||
| description={t("optional_guest_team_members_description")} | ||
| childrenClassName={classNames("lg:ml-0")} | ||
| switchContainerClassName={classNames( | ||
| "border-subtle rounded-lg border py-6 px-4 sm:px-6", | ||
| isGuestTeamMembersEnabled && "rounded-b-none" | ||
| )} | ||
| labelClassName={classNames("text-sm")} | ||
| checked={isGuestTeamMembersEnabled} | ||
| toggleSwitchAtTheEnd={true} | ||
| onCheckedChange={(checked) => { | ||
| setIsGuestTeamMembersEnabled(checked); | ||
| if (!checked) { | ||
| onChange([]); | ||
| } | ||
| }}> | ||
| <div className="border-subtle flex flex-col gap-4 rounded-b-lg border border-t-0 p-6"> | ||
| <CheckedTeamSelect | ||
| onChange={(options) => { | ||
| if (!onChange) return; | ||
| onChange(options.map((option) => ({ id: parseInt(option.value) }))); | ||
| }} | ||
| value={(addedGuestTeamMembers || []).reduce((acc, host) => { | ||
| const option = team.members?.find((member) => member.user.id === host.id); | ||
| if (!option) return acc; | ||
|
|
||
| acc.push({ | ||
| value: option.user.id.toString(), | ||
| avatar: option.user.avatarUrl || "", | ||
| label: option.user.email, | ||
| isFixed: true, | ||
| }); | ||
| return acc; | ||
| }, [] as CheckedSelectOption[])} | ||
| options={team?.members.map((member) => ({ | ||
| avatar: member.user.avatarUrl || "", | ||
| label: member.user.email || "", | ||
| value: member.user.id.toString() || "", | ||
| }))} | ||
| controlShouldRenderValue={false} | ||
| /> | ||
| </div> | ||
| </SettingsToggle> | ||
| </div> | ||
| )} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default GuestTeamMemberController; | ||
Uh oh!
There was an error while loading. Please reload this page.