)
- }, [getProviderIcon, inputValue, selectedCredentialProvider])
+ }, [
+ getProviderIcon,
+ inputValue,
+ selectedCredentialProvider,
+ isCredentialSetSelected,
+ selectedCredentialSet,
+ ])
const handleComboboxChange = useCallback(
(value: string) => {
@@ -214,6 +329,16 @@ export function CredentialSelector({
return
}
+ if (value.startsWith(CREDENTIAL_SET.PREFIX)) {
+ const credentialSetId = value.slice(CREDENTIAL_SET.PREFIX.length)
+ const matchedSet = credentialSets.find((cs) => cs.id === credentialSetId)
+ if (matchedSet) {
+ setInputValue(matchedSet.name)
+ handleCredentialSetSelect(credentialSetId)
+ return
+ }
+ }
+
const matchedCred = credentials.find((c) => c.id === value)
if (matchedCred) {
setInputValue(matchedCred.name)
@@ -224,15 +349,16 @@ export function CredentialSelector({
setIsEditing(true)
setInputValue(value)
},
- [credentials, handleAddCredential, handleSelect]
+ [credentials, credentialSets, handleAddCredential, handleSelect, handleCredentialSetSelect]
)
return (
{needsUpdate && (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector.tsx
index 761935d7d3..d202efb775 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector.tsx
@@ -10,6 +10,7 @@ import {
parseProvider,
} from '@/lib/oauth'
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal'
+import { CREDENTIAL } from '@/executor/constants'
import { useOAuthCredentialDetail, useOAuthCredentials } from '@/hooks/queries/oauth-credentials'
import { getMissingRequiredScopes } from '@/hooks/use-oauth-scope-status'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -95,7 +96,7 @@ export function ToolCredentialSelector({
const resolvedLabel = useMemo(() => {
if (selectedCredential) return selectedCredential.name
- if (isForeign) return 'Saved by collaborator'
+ if (isForeign) return CREDENTIAL.FOREIGN_LABEL
return ''
}, [selectedCredential, isForeign])
@@ -210,7 +211,7 @@ export function ToolCredentialSelector({
placeholder={label}
disabled={disabled}
editable={true}
- filterOptions={true}
+ filterOptions={!isForeign}
isLoading={credentialsLoading}
overlayContent={overlayContent}
className={selectedId ? 'pl-[28px]' : ''}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credential-sets/credential-sets.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credential-sets/credential-sets.tsx
new file mode 100644
index 0000000000..5fadc9e7bd
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credential-sets/credential-sets.tsx
@@ -0,0 +1,1170 @@
+'use client'
+
+import { type KeyboardEvent, useCallback, useMemo, useRef, useState } from 'react'
+import { createLogger } from '@sim/logger'
+import { Paperclip, Plus, Search, X } from 'lucide-react'
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+ Badge,
+ Button,
+ Input,
+ Label,
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+} from '@/components/emcn'
+import { GmailIcon, OutlookIcon } from '@/components/icons'
+import { Input as BaseInput, Skeleton } from '@/components/ui'
+import { useSession } from '@/lib/auth/auth-client'
+import { getSubscriptionStatus } from '@/lib/billing/client'
+import { cn } from '@/lib/core/utils/cn'
+import { getProviderDisplayName, type PollingProvider } from '@/lib/credential-sets/providers'
+import { quickValidateEmail } from '@/lib/messaging/email/validation'
+import { getUserRole } from '@/lib/workspaces/organization'
+import { EmailTag } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components'
+import { getUserColor } from '@/app/workspace/[workspaceId]/w/utils/get-user-color'
+import {
+ type CredentialSet,
+ useAcceptCredentialSetInvitation,
+ useCancelCredentialSetInvitation,
+ useCreateCredentialSet,
+ useCreateCredentialSetInvitation,
+ useCredentialSetInvitations,
+ useCredentialSetInvitationsDetail,
+ useCredentialSetMembers,
+ useCredentialSetMemberships,
+ useCredentialSets,
+ useDeleteCredentialSet,
+ useLeaveCredentialSet,
+ useRemoveCredentialSetMember,
+ useResendCredentialSetInvitation,
+} from '@/hooks/queries/credential-sets'
+import { useOrganizations } from '@/hooks/queries/organization'
+import { useSubscriptionData } from '@/hooks/queries/subscription'
+
+const logger = createLogger('EmailPolling')
+
+function CredentialSetsSkeleton() {
+ return (
+
+ )
+}
+
+export function CredentialSets() {
+ const { data: session } = useSession()
+ const { data: organizationsData } = useOrganizations()
+ const { data: subscriptionData } = useSubscriptionData()
+
+ const activeOrganization = organizationsData?.activeOrganization
+ const subscriptionStatus = getSubscriptionStatus(subscriptionData?.data)
+ const hasTeamPlan = subscriptionStatus.isTeam || subscriptionStatus.isEnterprise
+ const userRole = getUserRole(activeOrganization, session?.user?.email)
+ const isAdmin = userRole === 'admin' || userRole === 'owner'
+ const canManageCredentialSets = hasTeamPlan && isAdmin && !!activeOrganization?.id
+
+ const { data: memberships = [], isPending: membershipsLoading } = useCredentialSetMemberships()
+ const { data: invitations = [], isPending: invitationsLoading } = useCredentialSetInvitations()
+ const { data: ownedSets = [], isPending: ownedSetsLoading } = useCredentialSets(
+ activeOrganization?.id,
+ canManageCredentialSets
+ )
+
+ const acceptInvitation = useAcceptCredentialSetInvitation()
+ const createCredentialSet = useCreateCredentialSet()
+ const createInvitation = useCreateCredentialSetInvitation()
+
+ const [searchTerm, setSearchTerm] = useState('')
+ const [showCreateModal, setShowCreateModal] = useState(false)
+ const [viewingSet, setViewingSet] = useState
(null)
+ const [newSetName, setNewSetName] = useState('')
+ const [newSetDescription, setNewSetDescription] = useState('')
+ const [newSetProvider, setNewSetProvider] = useState('google-email')
+ const [createError, setCreateError] = useState(null)
+ const [emails, setEmails] = useState([])
+ const [invalidEmails, setInvalidEmails] = useState([])
+ const [duplicateEmails, setDuplicateEmails] = useState([])
+ const [inputValue, setInputValue] = useState('')
+ const [isDragging, setIsDragging] = useState(false)
+ const fileInputRef = useRef(null)
+ const [leavingMembership, setLeavingMembership] = useState<{
+ credentialSetId: string
+ name: string
+ } | null>(null)
+
+ const { data: members = [], isPending: membersLoading } = useCredentialSetMembers(viewingSet?.id)
+ const { data: pendingInvitations = [], isPending: pendingInvitationsLoading } =
+ useCredentialSetInvitationsDetail(viewingSet?.id)
+ const removeMember = useRemoveCredentialSetMember()
+ const leaveCredentialSet = useLeaveCredentialSet()
+ const deleteCredentialSet = useDeleteCredentialSet()
+ const cancelInvitation = useCancelCredentialSetInvitation()
+ const resendInvitation = useResendCredentialSetInvitation()
+
+ const [deletingSet, setDeletingSet] = useState<{ id: string; name: string } | null>(null)
+ const [deletingSetIds, setDeletingSetIds] = useState>(new Set())
+ const [cancellingInvitations, setCancellingInvitations] = useState>(new Set())
+ const [resendingInvitations, setResendingInvitations] = useState>(new Set())
+ const [resendCooldowns, setResendCooldowns] = useState>({})
+
+ const extractEmailsFromText = useCallback((text: string): string[] => {
+ const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g
+ const matches = text.match(emailRegex) || []
+ return [...new Set(matches.map((e) => e.toLowerCase()))]
+ }, [])
+
+ const addEmail = useCallback(
+ (email: string) => {
+ if (!email.trim()) return false
+
+ const normalized = email.trim().toLowerCase()
+ const validation = quickValidateEmail(normalized)
+ const isValid = validation.isValid
+
+ if (
+ emails.includes(normalized) ||
+ invalidEmails.includes(normalized) ||
+ duplicateEmails.includes(normalized)
+ ) {
+ return false
+ }
+
+ const isPendingInvitation = pendingInvitations.some(
+ (inv) => inv.email?.toLowerCase() === normalized
+ )
+ if (isPendingInvitation) {
+ setDuplicateEmails((prev) => {
+ if (prev.includes(normalized)) return prev
+ return [...prev, normalized]
+ })
+ setInputValue('')
+ return false
+ }
+
+ const isActiveMember = members.some(
+ (m) => m.userEmail?.toLowerCase() === normalized && m.status === 'active'
+ )
+ if (isActiveMember) {
+ setDuplicateEmails((prev) => {
+ if (prev.includes(normalized)) return prev
+ return [...prev, normalized]
+ })
+ setInputValue('')
+ return false
+ }
+
+ if (!isValid) {
+ setInvalidEmails((prev) => {
+ if (prev.includes(normalized)) return prev
+ return [...prev, normalized]
+ })
+ setInputValue('')
+ return false
+ }
+
+ setEmails((prev) => {
+ if (prev.includes(normalized)) return prev
+ return [...prev, normalized]
+ })
+ setInputValue('')
+ return true
+ },
+ [emails, invalidEmails, duplicateEmails, pendingInvitations, members]
+ )
+
+ const removeEmail = useCallback((index: number) => {
+ setEmails((prev) => prev.filter((_, i) => i !== index))
+ }, [])
+
+ const removeInvalidEmail = useCallback((index: number) => {
+ setInvalidEmails((prev) => prev.filter((_, i) => i !== index))
+ }, [])
+
+ const removeDuplicateEmail = useCallback((index: number) => {
+ setDuplicateEmails((prev) => prev.filter((_, i) => i !== index))
+ }, [])
+
+ const handleEmailKeyDown = useCallback(
+ (e: KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ e.preventDefault()
+ if (inputValue.trim()) {
+ addEmail(inputValue)
+ }
+ return
+ }
+
+ if ([',', ' '].includes(e.key) && inputValue.trim()) {
+ e.preventDefault()
+ addEmail(inputValue)
+ }
+
+ if (e.key === 'Backspace' && !inputValue) {
+ if (duplicateEmails.length > 0) {
+ removeDuplicateEmail(duplicateEmails.length - 1)
+ } else if (invalidEmails.length > 0) {
+ removeInvalidEmail(invalidEmails.length - 1)
+ } else if (emails.length > 0) {
+ removeEmail(emails.length - 1)
+ }
+ }
+ },
+ [
+ inputValue,
+ addEmail,
+ duplicateEmails,
+ invalidEmails,
+ emails,
+ removeDuplicateEmail,
+ removeInvalidEmail,
+ removeEmail,
+ ]
+ )
+
+ const handleEmailPaste = useCallback(
+ (e: React.ClipboardEvent) => {
+ e.preventDefault()
+ const pastedText = e.clipboardData.getData('text')
+ const pastedEmails = extractEmailsFromText(pastedText)
+
+ pastedEmails.forEach((email) => {
+ addEmail(email)
+ })
+ },
+ [addEmail, extractEmailsFromText]
+ )
+
+ const handleFileDrop = useCallback(
+ async (file: File) => {
+ try {
+ const text = await file.text()
+ const extractedEmails = extractEmailsFromText(text)
+ extractedEmails.forEach((email) => {
+ addEmail(email)
+ })
+ } catch (error) {
+ logger.error('Error reading dropped file', error)
+ }
+ },
+ [extractEmailsFromText, addEmail]
+ )
+
+ const handleDragOver = useCallback((e: React.DragEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+ e.dataTransfer.dropEffect = 'copy'
+ setIsDragging(true)
+ }, [])
+
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+ setIsDragging(false)
+ }, [])
+
+ const handleDrop = useCallback(
+ async (e: React.DragEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+ setIsDragging(false)
+
+ const files = Array.from(e.dataTransfer.files)
+ const validFiles = files.filter(
+ (f) =>
+ f.type === 'text/csv' ||
+ f.type === 'text/plain' ||
+ f.name.endsWith('.csv') ||
+ f.name.endsWith('.txt')
+ )
+
+ for (const file of validFiles) {
+ await handleFileDrop(file)
+ }
+ },
+ [handleFileDrop]
+ )
+
+ const handleFileInputChange = useCallback(
+ async (e: React.ChangeEvent) => {
+ const files = e.target.files
+ if (!files) return
+
+ for (const file of Array.from(files)) {
+ await handleFileDrop(file)
+ }
+
+ // Reset input so the same file can be selected again
+ e.target.value = ''
+ },
+ [handleFileDrop]
+ )
+
+ const handleRemoveMember = useCallback(
+ async (memberId: string) => {
+ if (!viewingSet) return
+ try {
+ await removeMember.mutateAsync({
+ credentialSetId: viewingSet.id,
+ memberId,
+ })
+ } catch (error) {
+ logger.error('Failed to remove member', error)
+ }
+ },
+ [viewingSet, removeMember]
+ )
+
+ const handleLeave = useCallback((credentialSetId: string, name: string) => {
+ setLeavingMembership({ credentialSetId, name })
+ }, [])
+
+ const confirmLeave = useCallback(async () => {
+ if (!leavingMembership) return
+ try {
+ await leaveCredentialSet.mutateAsync(leavingMembership.credentialSetId)
+ setLeavingMembership(null)
+ } catch (error) {
+ logger.error('Failed to leave polling group', error)
+ }
+ }, [leavingMembership, leaveCredentialSet])
+
+ const handleAcceptInvitation = useCallback(
+ async (token: string) => {
+ try {
+ await acceptInvitation.mutateAsync(token)
+ } catch (error) {
+ logger.error('Failed to accept invitation', error)
+ }
+ },
+ [acceptInvitation]
+ )
+
+ const handleCreateCredentialSet = useCallback(async () => {
+ if (!newSetName.trim() || !activeOrganization?.id) return
+ setCreateError(null)
+ try {
+ const result = await createCredentialSet.mutateAsync({
+ organizationId: activeOrganization.id,
+ name: newSetName.trim(),
+ description: newSetDescription.trim() || undefined,
+ providerId: newSetProvider,
+ })
+ setShowCreateModal(false)
+ setNewSetName('')
+ setNewSetDescription('')
+ setNewSetProvider('google-email')
+
+ // Open detail view for the newly created group
+ if (result?.credentialSet) {
+ setViewingSet(result.credentialSet)
+ }
+ } catch (error) {
+ logger.error('Failed to create polling group', error)
+ if (error instanceof Error) {
+ setCreateError(error.message)
+ } else {
+ setCreateError('Failed to create polling group')
+ }
+ }
+ }, [newSetName, newSetDescription, newSetProvider, activeOrganization?.id, createCredentialSet])
+
+ const handleInviteMembers = useCallback(async () => {
+ if (!viewingSet?.id) return
+
+ // Add any pending input value first
+ if (inputValue.trim()) {
+ addEmail(inputValue)
+ }
+
+ if (emails.length === 0) return
+
+ try {
+ for (const email of emails) {
+ await createInvitation.mutateAsync({
+ credentialSetId: viewingSet.id,
+ email,
+ })
+ }
+ setEmails([])
+ setInvalidEmails([])
+ setDuplicateEmails([])
+ setInputValue('')
+ } catch (error) {
+ logger.error('Failed to create invitations', error)
+ }
+ }, [viewingSet?.id, emails, inputValue, addEmail, createInvitation])
+
+ const handleCloseCreateModal = useCallback(() => {
+ setShowCreateModal(false)
+ setNewSetName('')
+ setNewSetDescription('')
+ setNewSetProvider('google-email')
+ setCreateError(null)
+ }, [])
+
+ const handleBackToList = useCallback(() => {
+ setViewingSet(null)
+ setEmails([])
+ setInvalidEmails([])
+ setDuplicateEmails([])
+ setInputValue('')
+ }, [])
+
+ const handleCancelInvitation = useCallback(
+ async (invitationId: string) => {
+ if (!viewingSet?.id) return
+
+ setCancellingInvitations((prev) => new Set([...prev, invitationId]))
+ try {
+ await cancelInvitation.mutateAsync({
+ credentialSetId: viewingSet.id,
+ invitationId,
+ })
+ } catch (error) {
+ logger.error('Failed to cancel invitation', error)
+ } finally {
+ setCancellingInvitations((prev) => {
+ const next = new Set(prev)
+ next.delete(invitationId)
+ return next
+ })
+ }
+ },
+ [viewingSet?.id, cancelInvitation]
+ )
+
+ const handleResendInvitation = useCallback(
+ async (invitationId: string, email: string) => {
+ if (!viewingSet?.id) return
+
+ const secondsLeft = resendCooldowns[invitationId]
+ if (secondsLeft && secondsLeft > 0) return
+
+ setResendingInvitations((prev) => new Set([...prev, invitationId]))
+ try {
+ await resendInvitation.mutateAsync({
+ credentialSetId: viewingSet.id,
+ invitationId,
+ email,
+ })
+
+ // Start 60s cooldown
+ setResendCooldowns((prev) => ({ ...prev, [invitationId]: 60 }))
+ const interval = setInterval(() => {
+ setResendCooldowns((prev) => {
+ const current = prev[invitationId]
+ if (current === undefined) return prev
+ if (current <= 1) {
+ const next = { ...prev }
+ delete next[invitationId]
+ clearInterval(interval)
+ return next
+ }
+ return { ...prev, [invitationId]: current - 1 }
+ })
+ }, 1000)
+ } catch (error) {
+ logger.error('Failed to resend invitation', error)
+ } finally {
+ setResendingInvitations((prev) => {
+ const next = new Set(prev)
+ next.delete(invitationId)
+ return next
+ })
+ }
+ },
+ [viewingSet?.id, resendInvitation, resendCooldowns]
+ )
+
+ const handleDeleteClick = useCallback((set: CredentialSet) => {
+ setDeletingSet({ id: set.id, name: set.name })
+ }, [])
+
+ const confirmDelete = useCallback(async () => {
+ if (!deletingSet || !activeOrganization?.id) return
+ setDeletingSetIds((prev) => new Set(prev).add(deletingSet.id))
+ try {
+ await deleteCredentialSet.mutateAsync({
+ credentialSetId: deletingSet.id,
+ organizationId: activeOrganization.id,
+ })
+ setDeletingSet(null)
+ } catch (error) {
+ logger.error('Failed to delete polling group', error)
+ } finally {
+ setDeletingSetIds((prev) => {
+ const next = new Set(prev)
+ next.delete(deletingSet.id)
+ return next
+ })
+ }
+ }, [deletingSet, activeOrganization?.id, deleteCredentialSet])
+
+ const getProviderIcon = (providerId: string | null) => {
+ if (providerId === 'outlook') return
+ return
+ }
+
+ // All hooks must be called before any early returns
+ const activeMemberships = useMemo(
+ () => memberships.filter((m) => m.status === 'active'),
+ [memberships]
+ )
+
+ const filteredInvitations = useMemo(() => {
+ if (!searchTerm.trim()) return invitations
+ const searchLower = searchTerm.toLowerCase()
+ return invitations.filter(
+ (inv) =>
+ inv.credentialSetName.toLowerCase().includes(searchLower) ||
+ inv.organizationName.toLowerCase().includes(searchLower)
+ )
+ }, [invitations, searchTerm])
+
+ const filteredMemberships = useMemo(() => {
+ if (!searchTerm.trim()) return activeMemberships
+ const searchLower = searchTerm.toLowerCase()
+ return activeMemberships.filter(
+ (m) =>
+ m.credentialSetName.toLowerCase().includes(searchLower) ||
+ m.organizationName.toLowerCase().includes(searchLower)
+ )
+ }, [activeMemberships, searchTerm])
+
+ const filteredOwnedSets = useMemo(() => {
+ if (!searchTerm.trim()) return ownedSets
+ const searchLower = searchTerm.toLowerCase()
+ return ownedSets.filter((set) => set.name.toLowerCase().includes(searchLower))
+ }, [ownedSets, searchTerm])
+
+ const hasNoContent =
+ invitations.length === 0 && activeMemberships.length === 0 && ownedSets.length === 0
+ const hasNoResults =
+ searchTerm.trim() &&
+ filteredInvitations.length === 0 &&
+ filteredMemberships.length === 0 &&
+ filteredOwnedSets.length === 0 &&
+ !hasNoContent
+
+ // Early returns AFTER all hooks
+ if (membershipsLoading || invitationsLoading) {
+ return
+ }
+
+ // Detail view for a polling group
+ if (viewingSet) {
+ const activeMembers = members.filter((m) => m.status === 'active')
+ const totalCount = activeMembers.length + pendingInvitations.length
+
+ return (
+ <>
+
+
+
+ {/* Group Info */}
+
+
+
+ Group Name
+
+
+ {viewingSet.name}
+
+
+
+
+
+ Provider
+
+
+ {getProviderIcon(viewingSet.providerId)}
+
+ {getProviderDisplayName(viewingSet.providerId as PollingProvider)}
+
+
+
+
+
+ {/* Invite Section - Email Tags Input */}
+
+
+
+ {isDragging && (
+
+
+ Drop file here
+
+
+ )}
+ {invalidEmails.map((email, index) => (
+
removeInvalidEmail(index)}
+ disabled={createInvitation.isPending}
+ isInvalid={true}
+ />
+ ))}
+ {duplicateEmails.map((email, index) => (
+
+ {email}
+ duplicate
+ {!createInvitation.isPending && (
+
+ )}
+
+ ))}
+ {emails.map((email, index) => (
+ removeEmail(index)}
+ disabled={createInvitation.isPending}
+ />
+ ))}
+
+
setInputValue(e.target.value)}
+ onKeyDown={handleEmailKeyDown}
+ onPaste={handleEmailPaste}
+ onBlur={() => inputValue.trim() && addEmail(inputValue)}
+ placeholder={
+ emails.length > 0 || invalidEmails.length > 0 || duplicateEmails.length > 0
+ ? 'Add another email'
+ : 'Enter email addresses'
+ }
+ className='h-6 min-w-[140px] flex-1 border-none bg-transparent p-0 pl-[4px] text-[13px] outline-none placeholder:text-[var(--text-tertiary)]'
+ disabled={createInvitation.isPending}
+ />
+
+
+
+
+
+
+ {/* Members List - styled like team members */}
+
+
Members
+
+ {membersLoading || pendingInvitationsLoading ? (
+
+ {[1, 2].map((i) => (
+
+ ))}
+
+ ) : totalCount === 0 ? (
+
+ No members yet. Send invitations above.
+
+ ) : (
+
+ {/* Active Members */}
+ {activeMembers.map((member) => {
+ const name = member.userName || 'Unknown'
+ const avatarInitial = name.charAt(0).toUpperCase()
+
+ return (
+
+
+
+ {member.userImage && (
+
+ )}
+
+ {avatarInitial}
+
+
+
+
+
+
+ {name}
+
+ {member.credentials.length === 0 && (
+
+ Disconnected
+
+ )}
+
+
+ {member.userEmail}
+
+
+
+
+
+
+
+
+ )
+ })}
+
+ {/* Pending Invitations */}
+ {pendingInvitations.map((invitation) => {
+ const email = invitation.email || 'Unknown'
+ const emailPrefix = email.split('@')[0]
+ const avatarInitial = emailPrefix.charAt(0).toUpperCase()
+
+ return (
+
+
+
+
+ {avatarInitial}
+
+
+
+
+
+
+ {emailPrefix}
+
+
+ Pending
+
+
+
+ {email}
+
+
+
+
+
+
+
+
+
+ )
+ })}
+
+ )}
+
+
+
+
+ {/* Footer Actions */}
+
+
+
+
+ >
+ )
+ }
+
+ return (
+ <>
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
+ />
+
+ {canManageCredentialSets && (
+
+ )}
+
+
+
+ {hasNoContent && !canManageCredentialSets ? (
+
+ You're not a member of any polling groups yet. When someone invites you, it will
+ appear here.
+
+ ) : hasNoResults ? (
+
+ No results found matching "{searchTerm}"
+
+ ) : (
+
+ {filteredInvitations.length > 0 && (
+
+
+ Pending Invitations
+
+ {filteredInvitations.map((invitation) => (
+
+
+
+ {getProviderIcon(invitation.providerId)}
+
+
+
+ {invitation.credentialSetName}
+
+
+ {invitation.organizationName}
+
+
+
+
+
+ ))}
+
+ )}
+
+ {filteredMemberships.length > 0 && (
+
+
+ My Memberships
+
+ {filteredMemberships.map((membership) => (
+
+
+
+ {getProviderIcon(membership.providerId)}
+
+
+
+ {membership.credentialSetName}
+
+
+ {membership.organizationName}
+
+
+
+
+
+ ))}
+
+ )}
+
+ {canManageCredentialSets &&
+ (filteredOwnedSets.length > 0 ||
+ ownedSetsLoading ||
+ (!searchTerm.trim() && ownedSets.length === 0)) && (
+
+
+ Manage
+
+ {ownedSetsLoading ? (
+ <>
+ {[1, 2].map((i) => (
+
+ ))}
+ >
+ ) : !searchTerm.trim() && ownedSets.length === 0 ? (
+
+ No polling groups created yet
+
+ ) : (
+ filteredOwnedSets.map((set) => (
+
+
+
+ {getProviderIcon(set.providerId)}
+
+
+ {set.name}
+
+ {set.memberCount} member{set.memberCount !== 1 ? 's' : ''}
+
+
+
+
+
+
+
+
+ ))
+ )}
+
+ )}
+
+ )}
+
+
+
+ {/* Create Polling Group Modal */}
+
+
+ Create Polling Group
+
+
+
+
+ {
+ setNewSetName(e.target.value)
+ if (createError) setCreateError(null)
+ }}
+ placeholder='e.g., Marketing Team'
+ />
+
+
+
+ setNewSetDescription(e.target.value)}
+ placeholder='e.g., Poll emails for marketing automations'
+ />
+
+
+
+
+
+
+
+
+ Members will connect their {getProviderDisplayName(newSetProvider)} account
+
+
+ {createError &&
{createError}
}
+
+
+
+
+
+
+
+
+
+ {/* Leave Confirmation Modal */}
+ setLeavingMembership(null)}>
+
+ Leave Polling Group
+
+
+ Are you sure you want to leave{' '}
+
+ {leavingMembership?.name}
+
+ ? Your email account will no longer be polled in workflows using this group.
+
+
+
+
+
+
+
+
+
+ {/* Delete Confirmation Modal */}
+ setDeletingSet(null)}>
+
+ Delete Polling Group
+
+
+ Are you sure you want to delete{' '}
+ {deletingSet?.name}?{' '}
+ This action cannot be undone.
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts
index 2c4d03228d..af86138a71 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts
@@ -1,6 +1,7 @@
export { ApiKeys } from './api-keys/api-keys'
export { BYOK } from './byok/byok'
export { Copilot } from './copilot/copilot'
+export { CredentialSets } from './credential-sets/credential-sets'
export { CustomTools } from './custom-tools/custom-tools'
export { EnvironmentVariables } from './environment/environment'
export { Files as FileUploads } from './files/files'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-invitation-card/member-invitation-card.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-invitation-card/member-invitation-card.tsx
index 6d3edbd64f..e176e813e5 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-invitation-card/member-invitation-card.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-invitation-card/member-invitation-card.tsx
@@ -1,7 +1,7 @@
'use client'
import React, { useMemo, useState } from 'react'
-import { CheckCircle, ChevronDown } from 'lucide-react'
+import { ChevronDown } from 'lucide-react'
import {
Button,
Checkbox,
@@ -302,14 +302,11 @@ export function MemberInvitationCard({
{/* Success message */}
{inviteSuccess && (
-
-
-
- Invitation sent successfully
- {selectedCount > 0 &&
- ` with access to ${selectedCount} workspace${selectedCount !== 1 ? 's' : ''}`}
-
-
+
+ Invitation sent successfully
+ {selectedCount > 0 &&
+ ` with access to ${selectedCount} workspace${selectedCount !== 1 ? 's' : ''}`}
+
)}