InviteMemberButton.utils.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import * as z from 'zod'
  2. import type { OrganizationMember } from '@/data/organizations/organization-members-query'
  3. export const MAX_BATCH_INVITE_SIZE = 50
  4. /** Max characters to show when an invalid token is long (e.g. comma-less paste of many addresses). */
  5. const MAX_INVALID_EMAIL_SNIPPET_LENGTH = 120
  6. function formatInvalidEmailSnippet(token: string): string {
  7. if (token.length <= MAX_INVALID_EMAIL_SNIPPET_LENGTH) return token
  8. return `${token.slice(0, MAX_INVALID_EMAIL_SNIPPET_LENGTH)}…`
  9. }
  10. export const emailSchema = z
  11. .string()
  12. .min(1, 'At least one email address is required')
  13. .refine(
  14. (val) => {
  15. const emails = parseEmails(val)
  16. if (emails.length === 0) return false
  17. return emails.every((e) => z.string().email().safeParse(e).success)
  18. },
  19. (val) => {
  20. const emails = parseEmails(val)
  21. const invalid = emails.find((e) => !z.string().email().safeParse(e).success)
  22. return {
  23. message: invalid
  24. ? `Invalid email address: "${formatInvalidEmailSnippet(invalid)}"`
  25. : 'At least one email address is required',
  26. }
  27. }
  28. )
  29. .refine(
  30. (val) => parseEmails(val).length <= MAX_BATCH_INVITE_SIZE,
  31. (val) => {
  32. const count = parseEmails(val).length
  33. return {
  34. message: `You can invite up to ${MAX_BATCH_INVITE_SIZE} members at a time. Remove ${count - MAX_BATCH_INVITE_SIZE} email ${count - MAX_BATCH_INVITE_SIZE === 1 ? 'address' : 'addresses'} to continue.`,
  35. }
  36. }
  37. )
  38. export function parseEmails(value: string): string[] {
  39. const emails = value
  40. .split(/[\s,]+/)
  41. .map((e) => e.trim().toLowerCase())
  42. .filter(Boolean)
  43. return [...new Set(emails)]
  44. }
  45. export type CategorizedEmails = {
  46. alreadyInvited: string[]
  47. alreadyMembers: string[]
  48. toInvite: string[]
  49. }
  50. export type BatchInvitationFailure = {
  51. email: string
  52. error: string
  53. }
  54. export type BatchInvitationResult = {
  55. succeeded: string[]
  56. failed: BatchInvitationFailure[]
  57. }
  58. export function categorizeInviteEmails(
  59. emails: string[],
  60. members: OrganizationMember[]
  61. ): CategorizedEmails {
  62. const alreadyInvited: string[] = []
  63. const alreadyMembers: string[] = []
  64. const toInvite: string[] = []
  65. for (const email of emails) {
  66. const existingMember = members.find((m) => m.primary_email === email)
  67. if (existingMember !== undefined) {
  68. if (existingMember.invited_id) {
  69. alreadyInvited.push(email)
  70. } else {
  71. alreadyMembers.push(email)
  72. }
  73. } else {
  74. toInvite.push(email)
  75. }
  76. }
  77. return { alreadyInvited, alreadyMembers, toInvite }
  78. }
  79. export function buildProjectPayload(
  80. applyToOrg: boolean,
  81. projectRef: string
  82. ): { projects: string[] } | Record<string, never> {
  83. if (applyToOrg) return {}
  84. if (!projectRef) {
  85. throw new Error('projectRef is required when applyToOrg is false')
  86. }
  87. return { projects: [projectRef] }
  88. }
  89. export function buildSsoPayload(
  90. requireSso: 'auto' | 'sso' | 'non-sso'
  91. ): { requireSso: boolean } | Record<string, never> {
  92. if (requireSso === 'sso') return { requireSso: true }
  93. if (requireSso === 'non-sso') return { requireSso: false }
  94. return {}
  95. }