Skip to content

Commit e96293a

Browse files
author
Claude Assistant
committed
fix: address Cubic Dev AI P2 review issues
- Use Promise.all for parallel email invites (Issue 1) - Use i18n.language instead of hardcoded "en" (Issue 2) - Notify users about invalid emails instead of silently dropping (Issue 3) - Use opt.email instead of opt.label for deduplication (Issue 4)
1 parent 480972b commit e96293a

2 files changed

Lines changed: 53 additions & 28 deletions

File tree

apps/web/modules/event-types/components/AddMembersWithSwitch.tsx

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ const CheckedHostField = ({
8282
teamId?: number;
8383
allowEmailInvites?: boolean;
8484
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
85-
const { t } = useLocale();
85+
const { t, i18n } = useLocale();
8686
const inviteMutation = trpc.viewer.teams.inviteMember.useMutation();
8787

8888
// Handle inviting new members by email
@@ -92,28 +92,30 @@ const CheckedHostField = ({
9292
return { success: [], failed: emails };
9393
}
9494

95-
const success: string[] = [];
96-
const failed: string[] = [];
95+
// Process invites in parallel for better performance
96+
const results = await Promise.all(
97+
emails.map(async (email) => {
98+
try {
99+
await inviteMutation.mutateAsync({
100+
teamId,
101+
usernameOrEmail: email,
102+
role: MembershipRole.MEMBER,
103+
language: i18n.language,
104+
creationSource: CreationSource.WEBAPP,
105+
});
106+
return { email, success: true };
107+
} catch (error) {
108+
return { email, success: false };
109+
}
110+
})
111+
);
97112

98-
// Invite each email
99-
for (const email of emails) {
100-
try {
101-
await inviteMutation.mutateAsync({
102-
teamId,
103-
usernameOrEmail: email,
104-
role: MembershipRole.MEMBER,
105-
language: "en",
106-
creationSource: CreationSource.WEBAPP,
107-
});
108-
success.push(email);
109-
} catch (error) {
110-
failed.push(email);
111-
}
112-
}
113+
const success = results.filter((r) => r.success).map((r) => r.email);
114+
const failed = results.filter((r) => !r.success).map((r) => r.email);
113115

114116
return { success, failed };
115117
},
116-
[teamId, inviteMutation]
118+
[teamId, inviteMutation, i18n.language]
117119
);
118120

119121
return (

packages/features/eventtypes/components/CheckedTeamSelect.tsx

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,24 @@ const isValidEmail = (email: string): boolean => {
7373
};
7474

7575
// Parse pasted text for multiple emails
76-
const parsePastedEmails = (text: string): string[] => {
77-
return text
76+
const parsePastedEmails = (text: string): { valid: string[]; invalid: string[] } => {
77+
const tokens = text
7878
.split(/[,;\n\r\s]+/)
7979
.map((email) => email.trim().toLowerCase())
80-
.filter((email) => email.length > 0 && isValidEmail(email));
80+
.filter((email) => email.length > 0);
81+
82+
const valid: string[] = [];
83+
const invalid: string[] = [];
84+
85+
for (const email of tokens) {
86+
if (isValidEmail(email)) {
87+
valid.push(email);
88+
} else {
89+
invalid.push(email);
90+
}
91+
}
92+
93+
return { valid, invalid };
8194
};
8295

8396
export const CheckedTeamSelect = ({
@@ -117,14 +130,19 @@ export const CheckedTeamSelect = ({
117130
async (inputValue: string) => {
118131
if (!allowEmailInvites || !onEmailInvite) return;
119132

120-
const emails = parsePastedEmails(inputValue);
121-
if (emails.length === 0) {
133+
const { valid: emails, invalid: invalidEmails } = parsePastedEmails(inputValue);
134+
135+
// Notify user about invalid emails
136+
if (invalidEmails.length > 0) {
122137
showToast(t("invalid_email_format"), "error");
138+
}
139+
140+
if (emails.length === 0) {
123141
return;
124142
}
125143

126-
// Check which emails are already in options (by checking if label matches email)
127-
const existingEmails = new Set(options.map((opt) => opt.label?.toLowerCase()).filter(Boolean));
144+
// Check which emails are already in options (by checking if email matches)
145+
const existingEmails = new Set(options.map((opt) => opt.email?.toLowerCase()).filter(Boolean));
128146
const newEmails = emails.filter((email) => !existingEmails.has(email));
129147

130148
if (newEmails.length === 0) {
@@ -165,11 +183,16 @@ export const CheckedTeamSelect = ({
165183
if (!allowEmailInvites) return;
166184

167185
const pastedText = event.clipboardData.getData("text");
168-
const emails = parsePastedEmails(pastedText);
186+
const { valid: emails, invalid: invalidEmails } = parsePastedEmails(pastedText);
187+
188+
// Notify user about invalid emails
189+
if (invalidEmails.length > 0) {
190+
showToast(t("invalid_email_format"), "error");
191+
}
169192

170193
// If we have multiple emails or a valid single email that's not in options, process it
171194
if (emails.length >= 1) {
172-
const existingEmails = new Set(options.map((opt) => opt.label?.toLowerCase()).filter(Boolean));
195+
const existingEmails = new Set(options.map((opt) => opt.email?.toLowerCase()).filter(Boolean));
173196
const newEmails = emails.filter((email) => !existingEmails.has(email));
174197

175198
if (newEmails.length > 0 && onEmailInvite) {

0 commit comments

Comments
 (0)