feat(email): enhance email ui#176
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Important
Looks good to me! 👍
Reviewed everything up to 7b2f98b in 6 seconds. Click for details.
- Reviewed
242lines of code in2files - Skipped
2files when reviewing. - Skipped posting
0draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
Workflow ID: wflow_PQecnjIPEClxEoqv
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
📝 WalkthroughWalkthroughAdds a permissionLevel field to the workspace invitation flow and updates the invite email template to accept and render that permission level; the API now passes permissionLevel into the email template call and the email component was redesigned with a branded HTML layout and new props. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile OverviewGreptile Summary
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
autonumber
participant C as Client
participant N as Next.js API Route
participant A as Auth (requireAuthWithUserInfo)
participant DB as Drizzle DB
participant R as Resend
participant E as InviteEmailTemplate
C->>N: POST /api/workspaces/:id/collaborators {email, permissionLevel}
N->>A: requireAuthWithUserInfo()
A-->>N: currentUser
N->>DB: verifyWorkspaceAccess(workspaceId, currentUser, editor)
DB-->>N: ok
N->>DB: SELECT user by email DB-->>N: invitedUser? (may be null)
N->>DB: SELECT workspace (ws)
DB-->>N: ws
alt invitedUser exists
N->>DB: INSERT workspaceCollaborators
DB-->>N: newCollaborator
N->>E: Render email (inviterName, workspaceName, workspaceUrl, permissionLevel)
E-->>N: React email
N->>R: resend.emails.send(react email)
R-->>N: sent/err
N-->>C: 201 {collaborator}
else invitedUser missing
N->>DB: DELETE existing workspaceInvites
DB-->>N: ok
N->>DB: INSERT workspaceInvites(token, expiresAt, permissionLevel)
DB-->>N: ok
N->>E: Render email (inviterName, workspaceName, workspaceUrl?invite=token)
E-->>N: React email
N->>R: resend.emails.send(react email)
R-->>N: sent/err
N-->>C: 201 {pending: true}
end
|
| workspaceColor: ws.color || undefined, | ||
| permissionLevel, | ||
| }), | ||
| attachments: [logoAttachment], |
There was a problem hiding this comment.
logoAttachment is undefined - this will cause a runtime error when sending emails to non-existing users
| attachments: [logoAttachment], | |
| // attachments: [logoAttachment], // TODO: Define logoAttachment if needed |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/workspaces/[id]/collaborators/route.ts
Line: 279:279
Comment:
`logoAttachment` is undefined - this will cause a runtime error when sending emails to non-existing users
```suggestion
// attachments: [logoAttachment], // TODO: Define logoAttachment if needed
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
1 issue found across 4 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="src/components/email/invite-email.tsx">
<violation number="1" location="src/components/email/invite-email.tsx:13">
P3: `workspaceColor` was added to the template props but is never used in the component, so the email UI can’t reflect the workspace color. Either use it in the template or remove it until it’s wired in.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/app/api/workspaces/[id]/collaborators/route.ts (2)
194-214:⚠️ Potential issue | 🟠 Major
wscan beundefined— no null guard before property access.If the workspace row is missing (e.g., deleted between the access check and this query),
wswill beundefinedand accessingws.slug,ws.name,ws.coloron Lines 196, 201, 203–207 will throw aTypeError. The same issue exists on Line 264. Add a guard after the query on Line 155.Proposed guard
const [ws] = await db .select({ ... }) .from(workspaces) .where(eq(workspaces.id, workspaceId)) .limit(1); + if (!ws) { + return NextResponse.json({ error: "Workspace not found" }, { status: 404 }); + } + // If user exists, add as collaborator directly
242-260:⚠️ Potential issue | 🟡 MinorDelete + insert is not atomic — concurrent invites can race.
Two simultaneous invite requests for the same email could both delete and then both insert, resulting in duplicate rows, or one delete could remove the other's freshly inserted row. Wrap these in a transaction.
Wrap in a transaction
- await db - .delete(workspaceInvites) - .where( - and( - eq(workspaceInvites.workspaceId, workspaceId), - eq(workspaceInvites.email, email.trim().toLowerCase()) - ) - ); - - await db.insert(workspaceInvites).values({ - workspaceId, - email: email.trim().toLowerCase(), - token, - inviterId: currentUser.userId, - permissionLevel: permissionLevel === "viewer" ? "viewer" : "editor", - expiresAt: expiresAt.toISOString(), - }); + await db.transaction(async (tx) => { + await tx + .delete(workspaceInvites) + .where( + and( + eq(workspaceInvites.workspaceId, workspaceId), + eq(workspaceInvites.email, email.trim().toLowerCase()) + ) + ); + + await tx.insert(workspaceInvites).values({ + workspaceId, + email: email.trim().toLowerCase(), + token, + inviterId: currentUser.userId, + permissionLevel: permissionLevel === "viewer" ? "viewer" : "editor", + expiresAt: expiresAt.toISOString(), + }); + });
🤖 Fix all issues with AI agents
In `@src/app/api/workspaces/`[id]/collaborators/route.ts:
- Line 279: The code references logoAttachment in the pending-invite email path
(attachments: [logoAttachment]) but logoAttachment is not declared or imported,
causing a runtime ReferenceError; either remove the attachments field from the
email payload in the send-invite code path (so invitations send without an
attachment) or properly define/import logoAttachment by creating the attachment
object (e.g., reading the logo file and constructing the { filename,
content/type, cid } object) and use that variable; update the code paths that
build/send the pending-invite email (where attachments: [logoAttachment]
appears) to use the chosen fix and ensure the variable name logoAttachment is
defined in the same module or imported.
In `@src/components/email/invite-email.tsx`:
- Around line 17-22: The prop workspaceColor declared on
InviteEmailTemplateProps is passed by callers but not used in
InviteEmailTemplate; either destructure workspaceColor in the
InviteEmailTemplate function signature and apply it (e.g., set the CTA button
background or heading accent using workspaceColor) so the passed color affects
the rendered template, or remove workspaceColor from InviteEmailTemplateProps
and stop passing it from the route to eliminate dead code; update
InviteEmailTemplate (and any callers) accordingly to keep the prop and usage
consistent.
🧹 Nitpick comments (5)
src/components/email/invite-email.tsx (2)
23-23:roleTextsilently maps unexpected permission levels to "an editor".If
permissionLevelis anything other than'viewer'(e.g.,'owner','admin', or a typo), the email will say "as an editor." Consider an explicit allowlist or a fallback that omits the role text for unknown values.Proposed safer mapping
- const roleText = permissionLevel === 'viewer' ? 'a viewer' : 'an editor'; + const roleMap: Record<string, string> = { viewer: 'a viewer', editor: 'an editor' }; + const roleText = permissionLevel ? roleMap[permissionLevel] : undefined;Then on Line 71:
- {permissionLevel ? ` as ${roleText}` : ''}. + {roleText ? ` as ${roleText}` : ''}.
26-170: Consider wrapping with<html>,<head>, and<body>for broader email client compatibility.Many email clients (especially Outlook and older webmail) strip or re-wrap content that doesn't include a proper HTML document skeleton. Starting the template from a bare
<div>can lead to inconsistent rendering. A minimal<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>…</body></html>wrapper improves deliverability and rendering consistency.src/app/api/workspaces/[id]/collaborators/route.ts (3)
1-13: Duplicate doc comment block.The file header comment (Lines 1–6) is repeated verbatim at Lines 8–13. Remove one of them.
Remove duplicate
/** * Collaborators API - List and invite collaborators * * GET /api/workspaces/[id]/collaborators - List collaborators * POST /api/workspaces/[id]/collaborators - Invite a new collaborator */ - -/** - * Collaborators API - List and invite collaborators - * - * GET /api/workspaces/[id]/collaborators - List collaborators - * POST /api/workspaces/[id]/collaborators - Invite a new collaborator - */
226-233:Math.randomfallback produces guessable invite tokens.The
cryptoglobal is available in all Node.js ≥ 19 and in the Web/Edge runtime, so the fallback should never be reached in Next.js 16. However, if it ever does,Math.randomtokens are trivially predictable and an attacker could forge invite links. Consider removing the fallback entirely and letting it fail loudly, or importingnode:cryptoas a guaranteed server-side alternative.Remove insecure fallback
const generateToken = () => { - if (typeof crypto !== 'undefined') { - const array = new Uint8Array(24); - crypto.getRandomValues(array); - return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join(''); - } else { - return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); - } + const array = new Uint8Array(24); + crypto.getRandomValues(array); + return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join(''); };
136-152: The"non-existent"sentinel is fragile — prefer skipping the query when user is not found.When
invitedUserisnull, passing"non-existent"to the equality check works by accident but is unclear and could theoretically collide (however unlikely). It's cleaner to skip the query entirely.Proposed improvement
- const [existing] = await db - .select({ id: workspaceCollaborators.id }) - .from(workspaceCollaborators) - .where( - and( - eq(workspaceCollaborators.workspaceId, workspaceId), - eq(workspaceCollaborators.userId, invitedUser?.id || "non-existent") - ) - ) - .limit(1); - - if (existing) { - return NextResponse.json( - { message: "User is already a collaborator" }, - { status: 409 } - ); - } + if (invitedUser) { + const [existing] = await db + .select({ id: workspaceCollaborators.id }) + .from(workspaceCollaborators) + .where( + and( + eq(workspaceCollaborators.workspaceId, workspaceId), + eq(workspaceCollaborators.userId, invitedUser.id) + ) + ) + .limit(1); + + if (existing) { + return NextResponse.json( + { message: "User is already a collaborator" }, + { status: 409 } + ); + } + }
| workspaceColor: ws.color || undefined, | ||
| permissionLevel, | ||
| }), | ||
| attachments: [logoAttachment], |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n 'logoAttachment' src/app/api/workspaces/[id]/collaborators/route.tsRepository: ThinkEx-OSS/thinkex
Length of output: 109
🏁 Script executed:
cat -n src/app/api/workspaces/[id]/collaborators/route.ts | head -30Repository: ThinkEx-OSS/thinkex
Length of output: 1245
🏁 Script executed:
cat -n src/app/api/workspaces/[id]/collaborators/route.ts | sed -n '260,290p'Repository: ThinkEx-OSS/thinkex
Length of output: 1366
🏁 Script executed:
wc -l src/app/api/workspaces/[id]/collaborators/route.tsRepository: ThinkEx-OSS/thinkex
Length of output: 117
🏁 Script executed:
cat -n src/app/api/workspaces/[id]/collaborators/route.ts | sed -n '1,50p'Repository: ThinkEx-OSS/thinkex
Length of output: 1978
logoAttachment is undefined — this will throw a ReferenceError at runtime.
logoAttachment is referenced on line 279 but never declared or imported in this file. When the pending-invite email path executes, this will crash the email send. Although the error is caught (line 281-284), the invitation email will silently fail to send, leaving users without notification of their invite. Either define/import logoAttachment (e.g., read a logo file and construct the attachment object), or remove the attachments field until it's implemented.
Quick fix: remove until properly implemented
react: InviteEmailTemplate({
inviterName: currentUser.name || 'A user',
workspaceName: ws.name || 'Workspace',
workspaceUrl,
workspaceColor: ws.color || undefined,
permissionLevel,
}),
- attachments: [logoAttachment],
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| attachments: [logoAttachment], | |
| react: InviteEmailTemplate({ | |
| inviterName: currentUser.name || 'A user', | |
| workspaceName: ws.name || 'Workspace', | |
| workspaceUrl, | |
| workspaceColor: ws.color || undefined, | |
| permissionLevel, | |
| }), | |
| }); |
🤖 Prompt for AI Agents
In `@src/app/api/workspaces/`[id]/collaborators/route.ts at line 279, The code
references logoAttachment in the pending-invite email path (attachments:
[logoAttachment]) but logoAttachment is not declared or imported, causing a
runtime ReferenceError; either remove the attachments field from the email
payload in the send-invite code path (so invitations send without an attachment)
or properly define/import logoAttachment by creating the attachment object
(e.g., reading the logo file and constructing the { filename, content/type, cid
} object) and use that variable; update the code paths that build/send the
pending-invite email (where attachments: [logoAttachment] appears) to use the
chosen fix and ensure the variable name logoAttachment is defined in the same
module or imported.
There was a problem hiding this comment.
Important
Looks good to me! 👍
Reviewed 22ab824 in 9 seconds. Click for details.
- Reviewed
54lines of code in2files - Skipped
0files when reviewing. - Skipped posting
0draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
Workflow ID: wflow_Q9TDtUIQKZvR10JB
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
|
@greptile-ai |
Additional Comments (2)
When |
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/app/api/workspaces/[id]/collaborators/route.ts (2)
266-275:⚠️ Potential issue | 🟡 Minor
permissionLevelis not passed to the pending-invite email template.The direct-invite path (Line 205) passes
permissionLeveltoInviteEmailTemplate, but the pending-invite path here does not. Pending invitees will see the email without their assigned role, producing an inconsistent experience.Proposed fix
react: InviteEmailTemplate({ inviterName: currentUser.name || 'A user', workspaceName: ws.name || 'Workspace', workspaceUrl, + permissionLevel, }),
155-163:⚠️ Potential issue | 🟡 Minor
wscould beundefined— unguarded access on Line 195 and 262.If no workspace is found (e.g., deleted between the access check and this query),
wswill beundefined. Line 195 (ws.slug) and Line 262 (ws.slug) would throw. Consider adding a guard or returning a 404 ifwsis missing.
🧹 Nitpick comments (1)
src/app/api/workspaces/[id]/collaborators/route.ts (1)
1-13: Duplicate doc comment block.The file-level JSDoc comment (lines 1–6) is repeated verbatim at lines 8–13.
|




Important
Enhances email UI for workspace invitations with redesigned templates, permission level display, and improved URL handling.
invite-email.tsx.route.ts.handlePOSTinroute.tsto includepermissionLevelin email data.This description was created by
for 22ab824. You can customize this summary. It will automatically update as commits are pushed.
Summary by CodeRabbit