mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-31 01:37:58 -05:00
Compare commits
3 Commits
fix/visibi
...
staging
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf2f1abcaf | ||
|
|
4109feecf6 | ||
|
|
37d5e01f5f |
@@ -264,7 +264,7 @@ async function handleToolsCall(
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ input: params.arguments || {}, triggerType: 'mcp' }),
|
||||
signal: AbortSignal.timeout(300000), // 5 minute timeout
|
||||
signal: AbortSignal.timeout(600000), // 10 minute timeout
|
||||
})
|
||||
|
||||
const executeResult = await response.json()
|
||||
|
||||
@@ -32,8 +32,7 @@ import {
|
||||
useTestNotification,
|
||||
useUpdateNotification,
|
||||
} from '@/hooks/queries/notifications'
|
||||
import { useConnectOAuthService } from '@/hooks/queries/oauth-connections'
|
||||
import { useSlackAccounts } from '@/hooks/use-slack-accounts'
|
||||
import { useConnectedAccounts, useConnectOAuthService } from '@/hooks/queries/oauth-connections'
|
||||
import { CORE_TRIGGER_TYPES, type CoreTriggerType } from '@/stores/logs/filters/types'
|
||||
import { SlackChannelSelector } from './components/slack-channel-selector'
|
||||
import { WorkflowSelector } from './components/workflow-selector'
|
||||
@@ -167,7 +166,8 @@ export function NotificationSettings({
|
||||
const deleteNotification = useDeleteNotification()
|
||||
const testNotification = useTestNotification()
|
||||
|
||||
const { accounts: slackAccounts, isLoading: isLoadingSlackAccounts } = useSlackAccounts()
|
||||
const { data: slackAccounts = [], isLoading: isLoadingSlackAccounts } =
|
||||
useConnectedAccounts('slack')
|
||||
const connectSlack = useConnectOAuthService()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -530,7 +530,7 @@ export function NotificationSettings({
|
||||
message:
|
||||
result.data?.error || (result.data?.success ? 'Test sent successfully' : 'Test failed'),
|
||||
})
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
setTestStatus({ id, success: false, message: 'Failed to send test' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useUserPermissions, type WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
|
||||
import {
|
||||
useWorkspacePermissions,
|
||||
useWorkspacePermissionsQuery,
|
||||
type WorkspacePermissions,
|
||||
} from '@/hooks/use-workspace-permissions'
|
||||
workspaceKeys,
|
||||
} from '@/hooks/queries/workspace'
|
||||
import { useUserPermissions, type WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
|
||||
import { useNotificationStore } from '@/stores/notifications'
|
||||
import { useOperationQueueStore } from '@/stores/operation-queue/store'
|
||||
|
||||
const logger = createLogger('WorkspacePermissionsProvider')
|
||||
|
||||
interface WorkspacePermissionsContextType {
|
||||
// Raw workspace permissions data
|
||||
workspacePermissions: WorkspacePermissions | null
|
||||
permissionsLoading: boolean
|
||||
permissionsError: string | null
|
||||
updatePermissions: (newPermissions: WorkspacePermissions) => void
|
||||
refetchPermissions: () => Promise<void>
|
||||
|
||||
// Computed user permissions (connection-aware)
|
||||
userPermissions: WorkspaceUserPermissions & { isOfflineMode?: boolean }
|
||||
|
||||
// Connection state management
|
||||
setOfflineMode: (isOffline: boolean) => void
|
||||
}
|
||||
|
||||
const WorkspacePermissionsContext = createContext<WorkspacePermissionsContextType>({
|
||||
@@ -43,7 +39,6 @@ const WorkspacePermissionsContext = createContext<WorkspacePermissionsContextTyp
|
||||
isLoading: false,
|
||||
error: null,
|
||||
},
|
||||
setOfflineMode: () => {},
|
||||
})
|
||||
|
||||
interface WorkspacePermissionsProviderProps {
|
||||
@@ -51,35 +46,20 @@ interface WorkspacePermissionsProviderProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider that manages workspace permissions and user access
|
||||
* Also provides connection-aware permissions that enforce read-only mode when offline
|
||||
* Provides workspace permissions and connection-aware user access throughout the app.
|
||||
* Enforces read-only mode when offline to prevent data loss.
|
||||
*/
|
||||
export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsProviderProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params?.workspaceId as string
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// Manage offline mode state locally
|
||||
const [isOfflineMode, setIsOfflineMode] = useState(false)
|
||||
|
||||
// Track whether we've already surfaced an offline notification to avoid duplicates
|
||||
const [hasShownOfflineNotification, setHasShownOfflineNotification] = useState(false)
|
||||
|
||||
// Get operation error state directly from the store (avoid full useCollaborativeWorkflow subscription)
|
||||
const hasOperationError = useOperationQueueStore((state) => state.hasOperationError)
|
||||
|
||||
const addNotification = useNotificationStore((state) => state.addNotification)
|
||||
|
||||
// Set offline mode when there are operation errors
|
||||
useEffect(() => {
|
||||
if (hasOperationError) {
|
||||
setIsOfflineMode(true)
|
||||
}
|
||||
}, [hasOperationError])
|
||||
const isOfflineMode = hasOperationError
|
||||
|
||||
/**
|
||||
* Surface a global notification when entering offline mode.
|
||||
* Uses the shared notifications system instead of bespoke UI in individual components.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isOfflineMode || hasShownOfflineNotification) {
|
||||
return
|
||||
@@ -89,7 +69,6 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP
|
||||
addNotification({
|
||||
level: 'error',
|
||||
message: 'Connection unavailable',
|
||||
// Global notification (no workflowId) so it is visible regardless of the active workflow
|
||||
action: {
|
||||
type: 'refresh',
|
||||
message: '',
|
||||
@@ -101,40 +80,44 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP
|
||||
}
|
||||
}, [addNotification, hasShownOfflineNotification, isOfflineMode])
|
||||
|
||||
// Fetch workspace permissions and loading state
|
||||
const {
|
||||
permissions: workspacePermissions,
|
||||
loading: permissionsLoading,
|
||||
error: permissionsError,
|
||||
updatePermissions,
|
||||
refetch: refetchPermissions,
|
||||
} = useWorkspacePermissions(workspaceId)
|
||||
data: workspacePermissions,
|
||||
isLoading: permissionsLoading,
|
||||
error: permissionsErrorObj,
|
||||
refetch,
|
||||
} = useWorkspacePermissionsQuery(workspaceId)
|
||||
|
||||
const permissionsError = permissionsErrorObj?.message ?? null
|
||||
|
||||
const updatePermissions = useCallback(
|
||||
(newPermissions: WorkspacePermissions) => {
|
||||
if (!workspaceId) return
|
||||
queryClient.setQueryData(workspaceKeys.permissions(workspaceId), newPermissions)
|
||||
},
|
||||
[workspaceId, queryClient]
|
||||
)
|
||||
|
||||
const refetchPermissions = useCallback(async () => {
|
||||
await refetch()
|
||||
}, [refetch])
|
||||
|
||||
// Get base user permissions from workspace permissions
|
||||
const baseUserPermissions = useUserPermissions(
|
||||
workspacePermissions,
|
||||
workspacePermissions ?? null,
|
||||
permissionsLoading,
|
||||
permissionsError
|
||||
)
|
||||
|
||||
// Note: Connection-based error detection removed - only rely on operation timeouts
|
||||
// The 5-second operation timeout system will handle all error cases
|
||||
|
||||
// Create connection-aware permissions that override user permissions when offline
|
||||
const userPermissions = useMemo((): WorkspaceUserPermissions & { isOfflineMode?: boolean } => {
|
||||
if (isOfflineMode) {
|
||||
// In offline mode, force read-only permissions regardless of actual user permissions
|
||||
return {
|
||||
...baseUserPermissions,
|
||||
canEdit: false,
|
||||
canAdmin: false,
|
||||
// Keep canRead true so users can still view content
|
||||
canRead: baseUserPermissions.canRead,
|
||||
isOfflineMode: true,
|
||||
}
|
||||
}
|
||||
|
||||
// When online, use normal permissions
|
||||
return {
|
||||
...baseUserPermissions,
|
||||
isOfflineMode: false,
|
||||
@@ -143,13 +126,12 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
workspacePermissions,
|
||||
workspacePermissions: workspacePermissions ?? null,
|
||||
permissionsLoading,
|
||||
permissionsError,
|
||||
updatePermissions,
|
||||
refetchPermissions,
|
||||
userPermissions,
|
||||
setOfflineMode: setIsOfflineMode,
|
||||
}),
|
||||
[
|
||||
workspacePermissions,
|
||||
@@ -169,8 +151,8 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access workspace permissions and data from context
|
||||
* This provides both raw workspace permissions and computed user permissions
|
||||
* Accesses workspace permissions data and operations from context.
|
||||
* Must be used within a WorkspacePermissionsProvider.
|
||||
*/
|
||||
export function useWorkspacePermissionsContext(): WorkspacePermissionsContextType {
|
||||
const context = useContext(WorkspacePermissionsContext)
|
||||
@@ -183,8 +165,8 @@ export function useWorkspacePermissionsContext(): WorkspacePermissionsContextTyp
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access user permissions from context
|
||||
* This replaces individual useUserPermissions calls and includes connection-aware permissions
|
||||
* Accesses the current user's computed permissions including offline mode status.
|
||||
* Convenience hook that extracts userPermissions from the context.
|
||||
*/
|
||||
export function useUserPermissionsContext(): WorkspaceUserPermissions & {
|
||||
isOfflineMode?: boolean
|
||||
|
||||
@@ -21,14 +21,13 @@ import { cn } from '@/lib/core/utils/cn'
|
||||
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
|
||||
import { getFileExtension } from '@/lib/uploads/utils/file-utils'
|
||||
import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components'
|
||||
import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import {
|
||||
useDeleteWorkspaceFile,
|
||||
useStorageInfo,
|
||||
useUploadWorkspaceFile,
|
||||
useWorkspaceFiles,
|
||||
} from '@/hooks/queries/workspace-files'
|
||||
import { useUserPermissions } from '@/hooks/use-user-permissions'
|
||||
import { useWorkspacePermissions } from '@/hooks/use-workspace-permissions'
|
||||
|
||||
const logger = createLogger('FileUploadsSettings')
|
||||
const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED'))
|
||||
@@ -94,9 +93,7 @@ export function Files() {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { permissions: workspacePermissions, loading: permissionsLoading } =
|
||||
useWorkspacePermissions(workspaceId)
|
||||
const userPermissions = useUserPermissions(workspacePermissions, permissionsLoading)
|
||||
const { userPermissions, permissionsLoading } = useWorkspacePermissionsContext()
|
||||
|
||||
const handleUploadClick = () => {
|
||||
fileInputRef.current?.click()
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
export const PermissionsTableSkeleton = React.memo(() => (
|
||||
<div className='scrollbar-hide max-h-[300px] overflow-y-auto'>
|
||||
<div className='flex items-center justify-between gap-[8px] py-[8px]'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Skeleton className='h-[14px] w-40 rounded-[4px]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-shrink-0 items-center'>
|
||||
<div className='inline-flex gap-[2px]'>
|
||||
<Skeleton className='h-[28px] w-[44px] rounded-[5px]' />
|
||||
<Skeleton className='h-[28px] w-[44px] rounded-[5px]' />
|
||||
<Skeleton className='h-[28px] w-[44px] rounded-[5px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
||||
PermissionsTableSkeleton.displayName = 'PermissionsTableSkeleton'
|
||||
@@ -1,20 +1,39 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Loader2, RotateCw, X } from 'lucide-react'
|
||||
import { Badge, Button, Tooltip } from '@/components/emcn'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { useSession } from '@/lib/auth/auth-client'
|
||||
import type { PermissionType } from '@/lib/workspaces/permissions/utils'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import type { WorkspacePermissions } from '@/hooks/use-workspace-permissions'
|
||||
import type { WorkspacePermissions } from '@/hooks/queries/workspace'
|
||||
import { PermissionSelector } from './permission-selector'
|
||||
import { PermissionsTableSkeleton } from './permissions-table-skeleton'
|
||||
import type { UserPermissions } from './types'
|
||||
|
||||
const PermissionsTableSkeleton = () => (
|
||||
<div className='scrollbar-hide max-h-[300px] overflow-y-auto'>
|
||||
<div className='flex items-center justify-between gap-[8px] py-[8px]'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Skeleton className='h-[14px] w-40 rounded-[4px]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-shrink-0 items-center'>
|
||||
<div className='inline-flex gap-[2px]'>
|
||||
<Skeleton className='h-[28px] w-[44px] rounded-[5px]' />
|
||||
<Skeleton className='h-[28px] w-[44px] rounded-[5px]' />
|
||||
<Skeleton className='h-[28px] w-[44px] rounded-[5px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export interface PermissionsTableProps {
|
||||
userPermissions: UserPermissions[]
|
||||
onPermissionChange: (userId: string, permissionType: PermissionType) => void
|
||||
onRemoveMember?: (userId: string, email: string) => void
|
||||
onRemoveInvitation?: (invitationId: string, email: string) => void
|
||||
onResendInvitation?: (invitationId: string, email: string) => void
|
||||
onResendInvitation?: (invitationId: string) => void
|
||||
disabled?: boolean
|
||||
existingUserPermissionChanges: Record<string, Partial<UserPermissions>>
|
||||
isSaving?: boolean
|
||||
@@ -143,7 +162,6 @@ export const PermissionsTable = ({
|
||||
<div>
|
||||
{allUsers.map((user) => {
|
||||
const isCurrentUser = user.isCurrentUser === true
|
||||
const isExistingUser = filteredExistingUsers.some((eu) => eu.email === user.email)
|
||||
const isPendingInvitation = user.isPendingInvitation === true
|
||||
const userIdentifier = user.userId || user.email
|
||||
const originalPermission = workspacePermissions?.users?.find(
|
||||
@@ -205,7 +223,7 @@ export const PermissionsTable = ({
|
||||
<span className='inline-flex'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => onResendInvitation(user.invitationId!, user.email)}
|
||||
onClick={() => onResendInvitation(user.invitationId!)}
|
||||
disabled={
|
||||
disabled ||
|
||||
isSaving ||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export { PermissionSelector } from './components/permission-selector'
|
||||
export { PermissionsTable } from './components/permissions-table'
|
||||
export { PermissionsTableSkeleton } from './components/permissions-table-skeleton'
|
||||
export type { PermissionType, UserPermissions } from './components/types'
|
||||
export { InviteModal } from './invite-modal'
|
||||
|
||||
@@ -19,7 +19,14 @@ import { useSession } from '@/lib/auth/auth-client'
|
||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||
import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import { PermissionsTable } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/permissions-table'
|
||||
import { API_ENDPOINTS } from '@/stores/constants'
|
||||
import {
|
||||
useBatchSendWorkspaceInvitations,
|
||||
useCancelWorkspaceInvitation,
|
||||
usePendingInvitations,
|
||||
useRemoveWorkspaceMember,
|
||||
useResendWorkspaceInvitation,
|
||||
useUpdateWorkspacePermissions,
|
||||
} from '@/hooks/queries/invitations'
|
||||
import type { PermissionType, UserPermissions } from './components/types'
|
||||
|
||||
const logger = createLogger('InviteModal')
|
||||
@@ -30,40 +37,25 @@ interface InviteModalProps {
|
||||
workspaceName?: string
|
||||
}
|
||||
|
||||
interface PendingInvitation {
|
||||
id: string
|
||||
workspaceId: string
|
||||
email: string
|
||||
permissions: PermissionType
|
||||
status: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalProps) {
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
const [emailItems, setEmailItems] = useState<TagItem[]>([])
|
||||
const [userPermissions, setUserPermissions] = useState<UserPermissions[]>([])
|
||||
const [pendingInvitations, setPendingInvitations] = useState<UserPermissions[]>([])
|
||||
const [isPendingInvitationsLoading, setIsPendingInvitationsLoading] = useState(false)
|
||||
const [existingUserPermissionChanges, setExistingUserPermissionChanges] = useState<
|
||||
Record<string, Partial<UserPermissions>>
|
||||
>({})
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const cooldownIntervalsRef = useRef<Map<string, NodeJS.Timeout>>(new Map())
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const [memberToRemove, setMemberToRemove] = useState<{ userId: string; email: string } | null>(
|
||||
null
|
||||
)
|
||||
const [isRemovingMember, setIsRemovingMember] = useState(false)
|
||||
const [invitationToRemove, setInvitationToRemove] = useState<{
|
||||
invitationId: string
|
||||
email: string
|
||||
} | null>(null)
|
||||
const [isRemovingInvitation, setIsRemovingInvitation] = useState(false)
|
||||
const [resendingInvitationIds, setResendingInvitationIds] = useState<Record<string, boolean>>({})
|
||||
const [resendCooldowns, setResendCooldowns] = useState<Record<string, number>>({})
|
||||
const [resentInvitationIds, setResentInvitationIds] = useState<Record<string, boolean>>({})
|
||||
const [resendingInvitationIds, setResendingInvitationIds] = useState<Record<string, boolean>>({})
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
|
||||
@@ -72,50 +64,26 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
workspacePermissions,
|
||||
permissionsLoading,
|
||||
updatePermissions,
|
||||
refetchPermissions,
|
||||
userPermissions: userPerms,
|
||||
} = useWorkspacePermissionsContext()
|
||||
|
||||
const { data: pendingInvitations = [], isLoading: isPendingInvitationsLoading } =
|
||||
usePendingInvitations(open ? workspaceId : undefined)
|
||||
|
||||
const batchSendInvitations = useBatchSendWorkspaceInvitations()
|
||||
const cancelInvitation = useCancelWorkspaceInvitation()
|
||||
const resendInvitation = useResendWorkspaceInvitation()
|
||||
const removeMember = useRemoveWorkspaceMember()
|
||||
const updatePermissionsMutation = useUpdateWorkspacePermissions()
|
||||
|
||||
const hasPendingChanges = Object.keys(existingUserPermissionChanges).length > 0
|
||||
const validEmails = emailItems.filter((item) => item.isValid).map((item) => item.value)
|
||||
const hasNewInvites = validEmails.length > 0
|
||||
|
||||
const fetchPendingInvitations = useCallback(async () => {
|
||||
if (!workspaceId) return
|
||||
|
||||
setIsPendingInvitationsLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/workspaces/invitations')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
const workspacePendingInvitations =
|
||||
data.invitations
|
||||
?.filter(
|
||||
(inv: PendingInvitation) =>
|
||||
inv.status === 'pending' && inv.workspaceId === workspaceId
|
||||
)
|
||||
.map((inv: PendingInvitation) => ({
|
||||
email: inv.email,
|
||||
permissionType: inv.permissions,
|
||||
isPendingInvitation: true,
|
||||
invitationId: inv.id,
|
||||
})) || []
|
||||
|
||||
setPendingInvitations(workspacePendingInvitations)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching pending invitations:', error)
|
||||
} finally {
|
||||
setIsPendingInvitationsLoading(false)
|
||||
}
|
||||
}, [workspaceId])
|
||||
|
||||
useEffect(() => {
|
||||
if (open && workspaceId) {
|
||||
fetchPendingInvitations()
|
||||
refetchPermissions()
|
||||
}
|
||||
}, [open, workspaceId, fetchPendingInvitations, refetchPermissions])
|
||||
const isSubmitting = batchSendInvitations.isPending
|
||||
const isSaving = updatePermissionsMutation.isPending
|
||||
const isRemovingMember = removeMember.isPending
|
||||
const isRemovingInvitation = cancelInvitation.isPending
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@@ -180,16 +148,12 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
[emailItems, pendingInvitations, workspacePermissions?.users, session?.user?.email]
|
||||
)
|
||||
|
||||
const removeEmailItem = useCallback(
|
||||
(_value: string, index: number, isValid?: boolean) => {
|
||||
const itemToRemove = emailItems[index]
|
||||
setEmailItems((prev) => prev.filter((_, i) => i !== index))
|
||||
if (isValid ?? itemToRemove?.isValid) {
|
||||
setUserPermissions((prev) => prev.filter((user) => user.email !== itemToRemove?.value))
|
||||
}
|
||||
},
|
||||
[emailItems]
|
||||
)
|
||||
const removeEmailItem = useCallback((value: string, index: number, isValid?: boolean) => {
|
||||
setEmailItems((prev) => prev.filter((_, i) => i !== index))
|
||||
if (isValid) {
|
||||
setUserPermissions((prev) => prev.filter((user) => user.email !== value))
|
||||
}
|
||||
}, [])
|
||||
|
||||
const fileInputOptions: FileInputOptions = useMemo(
|
||||
() => ({
|
||||
@@ -198,7 +162,8 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
extractValues: (text: 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 uniqueEmails = [...new Set(matches.map((e) => e.toLowerCase()))]
|
||||
return uniqueEmails.filter((email) => quickValidateEmail(email).isValid)
|
||||
},
|
||||
tooltip: 'Upload emails',
|
||||
}),
|
||||
@@ -230,53 +195,38 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
[workspacePermissions?.users]
|
||||
)
|
||||
|
||||
const handleSaveChanges = useCallback(async () => {
|
||||
const handleSaveChanges = useCallback(() => {
|
||||
if (!userPerms.canAdmin || !hasPendingChanges || !workspaceId) return
|
||||
|
||||
setIsSaving(true)
|
||||
setErrorMessage(null)
|
||||
|
||||
try {
|
||||
const updates = Object.entries(existingUserPermissionChanges).map(([userId, changes]) => ({
|
||||
userId,
|
||||
permissions: changes.permissionType || 'read',
|
||||
}))
|
||||
const updates = Object.entries(existingUserPermissionChanges).map(([userId, changes]) => ({
|
||||
userId,
|
||||
permissions: (changes.permissionType || 'read') as 'admin' | 'write' | 'read',
|
||||
}))
|
||||
|
||||
const response = await fetch(API_ENDPOINTS.WORKSPACE_PERMISSIONS(workspaceId), {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
updatePermissionsMutation.mutate(
|
||||
{ workspaceId, updates },
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
if (data.users && data.total !== undefined) {
|
||||
updatePermissions({ users: data.users, total: data.total })
|
||||
}
|
||||
setExistingUserPermissionChanges({})
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error('Error saving permission changes:', error)
|
||||
setErrorMessage(error.message || 'Failed to save permission changes. Please try again.')
|
||||
},
|
||||
body: JSON.stringify({ updates }),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to update permissions')
|
||||
}
|
||||
|
||||
if (data.users && data.total !== undefined) {
|
||||
updatePermissions({ users: data.users, total: data.total })
|
||||
}
|
||||
|
||||
setExistingUserPermissionChanges({})
|
||||
} catch (error) {
|
||||
logger.error('Error saving permission changes:', error)
|
||||
const errorMsg =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to save permission changes. Please try again.'
|
||||
setErrorMessage(errorMsg)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
)
|
||||
}, [
|
||||
userPerms.canAdmin,
|
||||
hasPendingChanges,
|
||||
workspaceId,
|
||||
existingUserPermissionChanges,
|
||||
updatePermissions,
|
||||
updatePermissionsMutation,
|
||||
])
|
||||
|
||||
const handleRestoreChanges = useCallback(() => {
|
||||
@@ -289,62 +239,57 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
setMemberToRemove({ userId, email })
|
||||
}, [])
|
||||
|
||||
const handleRemoveMemberConfirm = useCallback(async () => {
|
||||
const handleRemoveMemberConfirm = useCallback(() => {
|
||||
if (!memberToRemove || !workspaceId || !userPerms.canAdmin) return
|
||||
|
||||
setIsRemovingMember(true)
|
||||
setErrorMessage(null)
|
||||
|
||||
try {
|
||||
const userRecord = workspacePermissions?.users?.find(
|
||||
(user) => user.userId === memberToRemove.userId
|
||||
)
|
||||
const userRecord = workspacePermissions?.users?.find(
|
||||
(user) => user.userId === memberToRemove.userId
|
||||
)
|
||||
|
||||
if (!userRecord) {
|
||||
throw new Error('User is not a member of this workspace')
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/workspaces/members/${memberToRemove.userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
workspaceId: workspaceId,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to remove member')
|
||||
}
|
||||
|
||||
if (workspacePermissions) {
|
||||
const updatedUsers = workspacePermissions.users.filter(
|
||||
(user) => user.userId !== memberToRemove.userId
|
||||
)
|
||||
updatePermissions({
|
||||
users: updatedUsers,
|
||||
total: workspacePermissions.total - 1,
|
||||
})
|
||||
}
|
||||
|
||||
setExistingUserPermissionChanges((prev) => {
|
||||
const updated = { ...prev }
|
||||
delete updated[memberToRemove.userId]
|
||||
return updated
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error removing member:', error)
|
||||
const errorMsg =
|
||||
error instanceof Error ? error.message : 'Failed to remove member. Please try again.'
|
||||
setErrorMessage(errorMsg)
|
||||
} finally {
|
||||
setIsRemovingMember(false)
|
||||
if (!userRecord) {
|
||||
setErrorMessage('User is not a member of this workspace')
|
||||
setMemberToRemove(null)
|
||||
return
|
||||
}
|
||||
}, [memberToRemove, workspaceId, userPerms.canAdmin, workspacePermissions, updatePermissions])
|
||||
|
||||
removeMember.mutate(
|
||||
{ userId: memberToRemove.userId, workspaceId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
if (workspacePermissions) {
|
||||
const updatedUsers = workspacePermissions.users.filter(
|
||||
(user) => user.userId !== memberToRemove.userId
|
||||
)
|
||||
updatePermissions({
|
||||
users: updatedUsers,
|
||||
total: workspacePermissions.total - 1,
|
||||
})
|
||||
}
|
||||
|
||||
setExistingUserPermissionChanges((prev) => {
|
||||
const updated = { ...prev }
|
||||
delete updated[memberToRemove.userId]
|
||||
return updated
|
||||
})
|
||||
setMemberToRemove(null)
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error('Error removing member:', error)
|
||||
setErrorMessage(error.message || 'Failed to remove member. Please try again.')
|
||||
setMemberToRemove(null)
|
||||
},
|
||||
}
|
||||
)
|
||||
}, [
|
||||
memberToRemove,
|
||||
workspaceId,
|
||||
userPerms.canAdmin,
|
||||
workspacePermissions,
|
||||
updatePermissions,
|
||||
removeMember,
|
||||
])
|
||||
|
||||
const handleRemoveMemberCancel = useCallback(() => {
|
||||
setMemberToRemove(null)
|
||||
@@ -354,120 +299,101 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
setInvitationToRemove({ invitationId, email })
|
||||
}, [])
|
||||
|
||||
const handleRemoveInvitationConfirm = useCallback(async () => {
|
||||
const handleRemoveInvitationConfirm = useCallback(() => {
|
||||
if (!invitationToRemove || !workspaceId || !userPerms.canAdmin) return
|
||||
|
||||
setIsRemovingInvitation(true)
|
||||
setErrorMessage(null)
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/workspaces/invitations/${invitationToRemove.invitationId}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to cancel invitation')
|
||||
cancelInvitation.mutate(
|
||||
{ invitationId: invitationToRemove.invitationId, workspaceId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setInvitationToRemove(null)
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error('Error cancelling invitation:', error)
|
||||
setErrorMessage(error.message || 'Failed to cancel invitation. Please try again.')
|
||||
setInvitationToRemove(null)
|
||||
},
|
||||
}
|
||||
|
||||
setPendingInvitations((prev) =>
|
||||
prev.filter((inv) => inv.invitationId !== invitationToRemove.invitationId)
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error('Error cancelling invitation:', error)
|
||||
const errorMsg =
|
||||
error instanceof Error ? error.message : 'Failed to cancel invitation. Please try again.'
|
||||
setErrorMessage(errorMsg)
|
||||
} finally {
|
||||
setIsRemovingInvitation(false)
|
||||
setInvitationToRemove(null)
|
||||
}
|
||||
}, [invitationToRemove, workspaceId, userPerms.canAdmin])
|
||||
)
|
||||
}, [invitationToRemove, workspaceId, userPerms.canAdmin, cancelInvitation])
|
||||
|
||||
const handleRemoveInvitationCancel = useCallback(() => {
|
||||
setInvitationToRemove(null)
|
||||
}, [])
|
||||
|
||||
const handleResendInvitation = useCallback(
|
||||
async (invitationId: string, email: string) => {
|
||||
(invitationId: string) => {
|
||||
if (!workspaceId || !userPerms.canAdmin) return
|
||||
|
||||
const secondsLeft = resendCooldowns[invitationId]
|
||||
if (secondsLeft && secondsLeft > 0) return
|
||||
|
||||
setResendingInvitationIds((prev) => ({ ...prev, [invitationId]: true }))
|
||||
if (resendingInvitationIds[invitationId]) return
|
||||
|
||||
setErrorMessage(null)
|
||||
setResendingInvitationIds((prev) => ({ ...prev, [invitationId]: true }))
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/workspaces/invitations/${invitationId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to resend invitation')
|
||||
}
|
||||
|
||||
setResentInvitationIds((prev) => ({ ...prev, [invitationId]: true }))
|
||||
setTimeout(() => {
|
||||
setResentInvitationIds((prev) => {
|
||||
const next = { ...prev }
|
||||
delete next[invitationId]
|
||||
return next
|
||||
})
|
||||
}, 4000)
|
||||
} catch (error) {
|
||||
logger.error('Error resending invitation:', error)
|
||||
const errorMsg =
|
||||
error instanceof Error ? error.message : 'Failed to resend invitation. Please try again.'
|
||||
setErrorMessage(errorMsg)
|
||||
} finally {
|
||||
setResendingInvitationIds((prev) => {
|
||||
const next = { ...prev }
|
||||
delete next[invitationId]
|
||||
return next
|
||||
})
|
||||
setResendCooldowns((prev) => ({ ...prev, [invitationId]: 60 }))
|
||||
|
||||
const existingInterval = cooldownIntervalsRef.current.get(invitationId)
|
||||
if (existingInterval) {
|
||||
clearInterval(existingInterval)
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setResendCooldowns((prev) => {
|
||||
const current = prev[invitationId]
|
||||
if (current === undefined) return prev
|
||||
if (current <= 1) {
|
||||
resendInvitation.mutate(
|
||||
{ invitationId, workspaceId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setResendingInvitationIds((prev) => {
|
||||
const next = { ...prev }
|
||||
delete next[invitationId]
|
||||
clearInterval(interval)
|
||||
cooldownIntervalsRef.current.delete(invitationId)
|
||||
return next
|
||||
}
|
||||
return { ...prev, [invitationId]: current - 1 }
|
||||
})
|
||||
}, 1000)
|
||||
})
|
||||
setResentInvitationIds((prev) => ({ ...prev, [invitationId]: true }))
|
||||
setTimeout(() => {
|
||||
setResentInvitationIds((prev) => {
|
||||
const next = { ...prev }
|
||||
delete next[invitationId]
|
||||
return next
|
||||
})
|
||||
}, 4000)
|
||||
|
||||
cooldownIntervalsRef.current.set(invitationId, interval)
|
||||
}
|
||||
setResendCooldowns((prev) => ({ ...prev, [invitationId]: 60 }))
|
||||
|
||||
const existingInterval = cooldownIntervalsRef.current.get(invitationId)
|
||||
if (existingInterval) {
|
||||
clearInterval(existingInterval)
|
||||
}
|
||||
|
||||
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)
|
||||
cooldownIntervalsRef.current.delete(invitationId)
|
||||
return next
|
||||
}
|
||||
return { ...prev, [invitationId]: current - 1 }
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
cooldownIntervalsRef.current.set(invitationId, interval)
|
||||
},
|
||||
onError: (error) => {
|
||||
setResendingInvitationIds((prev) => {
|
||||
const next = { ...prev }
|
||||
delete next[invitationId]
|
||||
return next
|
||||
})
|
||||
logger.error('Error resending invitation:', error)
|
||||
setErrorMessage(error.message || 'Failed to resend invitation. Please try again.')
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
[workspaceId, userPerms.canAdmin, resendCooldowns]
|
||||
[workspaceId, userPerms.canAdmin, resendCooldowns, resendingInvitationIds, resendInvitation]
|
||||
)
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent) => {
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
setErrorMessage(null)
|
||||
@@ -476,122 +402,65 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
const failedInvites: string[] = []
|
||||
|
||||
const results = await Promise.all(
|
||||
validEmails.map(async (email) => {
|
||||
try {
|
||||
const userPermission = userPermissions.find((up) => up.email === email)
|
||||
const permissionType = userPermission?.permissionType || 'read'
|
||||
|
||||
const response = await fetch('/api/workspaces/invitations', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
workspaceId,
|
||||
email: email,
|
||||
role: 'member',
|
||||
permission: permissionType,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
failedInvites.push(email)
|
||||
|
||||
if (data.error) {
|
||||
setErrorMessage(data.error)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch {
|
||||
failedInvites.push(email)
|
||||
return false
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const successCount = results.filter(Boolean).length
|
||||
const successfulEmails = validEmails.filter((_, index) => results[index])
|
||||
|
||||
if (successCount > 0) {
|
||||
if (successfulEmails.length > 0) {
|
||||
const newPendingInvitations: UserPermissions[] = successfulEmails.map((email) => {
|
||||
const userPermission = userPermissions.find((up) => up.email === email)
|
||||
const permissionType = userPermission?.permissionType || 'read'
|
||||
|
||||
return {
|
||||
email,
|
||||
permissionType,
|
||||
isPendingInvitation: true,
|
||||
}
|
||||
})
|
||||
|
||||
setPendingInvitations((prev) => {
|
||||
const existingEmails = new Set(prev.map((inv) => inv.email))
|
||||
const merged = [...prev]
|
||||
|
||||
newPendingInvitations.forEach((inv) => {
|
||||
if (!existingEmails.has(inv.email)) {
|
||||
merged.push(inv)
|
||||
}
|
||||
})
|
||||
|
||||
return merged
|
||||
})
|
||||
}
|
||||
|
||||
fetchPendingInvitations()
|
||||
|
||||
if (failedInvites.length > 0) {
|
||||
setEmailItems(failedInvites.map((email) => ({ value: email, isValid: true })))
|
||||
setUserPermissions((prev) => prev.filter((user) => failedInvites.includes(user.email)))
|
||||
} else {
|
||||
setEmailItems([])
|
||||
setUserPermissions([])
|
||||
}
|
||||
const invitations = validEmails.map((email) => {
|
||||
const userPermission = userPermissions.find((up) => up.email === email)
|
||||
return {
|
||||
email,
|
||||
permission: (userPermission?.permissionType || 'read') as 'admin' | 'write' | 'read',
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Error inviting members:', err)
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : 'An unexpected error occurred. Please try again.'
|
||||
setErrorMessage(errorMessage)
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
})
|
||||
|
||||
batchSendInvitations.mutate(
|
||||
{ workspaceId, invitations },
|
||||
{
|
||||
onSuccess: (result) => {
|
||||
if (result.failed.length > 0) {
|
||||
setEmailItems(result.failed.map((f) => ({ value: f.email, isValid: true })))
|
||||
setUserPermissions((prev) =>
|
||||
prev.filter((user) => result.failed.some((f) => f.email === user.email))
|
||||
)
|
||||
setErrorMessage(result.failed[0].error)
|
||||
} else {
|
||||
setEmailItems([])
|
||||
setUserPermissions([])
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error('Error inviting members:', error)
|
||||
setErrorMessage(error.message || 'An unexpected error occurred. Please try again.')
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
[validEmails, workspaceId, userPermissions, fetchPendingInvitations]
|
||||
[validEmails, workspaceId, userPermissions, batchSendInvitations]
|
||||
)
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setEmailItems([])
|
||||
setUserPermissions([])
|
||||
setPendingInvitations([])
|
||||
setIsPendingInvitationsLoading(false)
|
||||
setExistingUserPermissionChanges({})
|
||||
setIsSubmitting(false)
|
||||
setIsSaving(false)
|
||||
setErrorMessage(null)
|
||||
setMemberToRemove(null)
|
||||
setIsRemovingMember(false)
|
||||
setInvitationToRemove(null)
|
||||
setIsRemovingInvitation(false)
|
||||
setResendCooldowns({})
|
||||
setResentInvitationIds({})
|
||||
setResendingInvitationIds({})
|
||||
|
||||
cooldownIntervalsRef.current.forEach((interval) => clearInterval(interval))
|
||||
cooldownIntervalsRef.current.clear()
|
||||
}, [])
|
||||
|
||||
const pendingInvitationsForTable: UserPermissions[] = useMemo(
|
||||
() =>
|
||||
pendingInvitations.map((inv) => ({
|
||||
email: inv.email,
|
||||
permissionType: inv.permissionType,
|
||||
isPendingInvitation: true,
|
||||
invitationId: inv.invitationId,
|
||||
})),
|
||||
[pendingInvitations]
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
@@ -681,7 +550,7 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
isSaving={isSaving}
|
||||
workspacePermissions={workspacePermissions}
|
||||
permissionsLoading={permissionsLoading}
|
||||
pendingInvitations={pendingInvitations}
|
||||
pendingInvitations={pendingInvitationsForTable}
|
||||
isPendingInvitationsLoading={isPendingInvitationsLoading}
|
||||
resendingInvitationIds={resendingInvitationIds}
|
||||
resentInvitationIds={resentInvitationIds}
|
||||
@@ -691,26 +560,29 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className='justify-between'>
|
||||
{hasPendingChanges && userPerms.canAdmin && (
|
||||
<div className='flex gap-[8px]'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='default'
|
||||
disabled={isSaving || isSubmitting}
|
||||
onClick={handleRestoreChanges}
|
||||
>
|
||||
Restore Changes
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='tertiary'
|
||||
disabled={isSaving || isSubmitting}
|
||||
onClick={handleSaveChanges}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`flex gap-[8px] ${hasPendingChanges && userPerms.canAdmin ? '' : 'pointer-events-none invisible'}`}
|
||||
aria-hidden={!(hasPendingChanges && userPerms.canAdmin)}
|
||||
>
|
||||
<Button
|
||||
type='button'
|
||||
variant='default'
|
||||
disabled={isSaving || isSubmitting}
|
||||
onClick={handleRestoreChanges}
|
||||
tabIndex={hasPendingChanges && userPerms.canAdmin ? 0 : -1}
|
||||
>
|
||||
Restore Changes
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='tertiary'
|
||||
disabled={isSaving || isSubmitting}
|
||||
onClick={handleSaveChanges}
|
||||
tabIndex={hasPendingChanges && userPerms.canAdmin ? 0 : -1}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type='button'
|
||||
|
||||
@@ -14,4 +14,4 @@ export {
|
||||
export { useSidebarResize } from './use-sidebar-resize'
|
||||
export { useWorkflowOperations } from './use-workflow-operations'
|
||||
export { useWorkflowSelection } from './use-workflow-selection'
|
||||
export { useWorkspaceManagement } from './use-workspace-management'
|
||||
export { useWorkspaceManagement, type Workspace } from './use-workspace-management'
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { generateWorkspaceName } from '@/lib/workspaces/naming'
|
||||
import { useLeaveWorkspace } from '@/hooks/queries/invitations'
|
||||
import {
|
||||
useCreateWorkspace,
|
||||
useDeleteWorkspace,
|
||||
useUpdateWorkspaceName,
|
||||
useWorkspacesQuery,
|
||||
type Workspace,
|
||||
workspaceKeys,
|
||||
} from '@/hooks/queries/workspace'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
const logger = createLogger('useWorkspaceManagement')
|
||||
|
||||
interface Workspace {
|
||||
id: string
|
||||
name: string
|
||||
ownerId: string
|
||||
role?: string
|
||||
membershipId?: string
|
||||
permissions?: 'admin' | 'write' | 'read' | null
|
||||
}
|
||||
|
||||
interface UseWorkspaceManagementProps {
|
||||
workspaceId: string
|
||||
sessionUserId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to manage workspace operations including fetching, switching, creating, deleting, and leaving workspaces.
|
||||
* Manages workspace operations including fetching, switching, creating, deleting, and leaving workspaces.
|
||||
* Handles workspace validation and URL synchronization.
|
||||
*
|
||||
* @param props - Configuration object containing workspaceId and sessionUserId
|
||||
* @returns Workspace management state and operations
|
||||
* @param props.workspaceId - The current workspace ID from the URL
|
||||
* @param props.sessionUserId - The current user's session ID
|
||||
* @returns Workspace state and operations
|
||||
*/
|
||||
export function useWorkspaceManagement({
|
||||
workspaceId,
|
||||
@@ -33,140 +35,68 @@ export function useWorkspaceManagement({
|
||||
}: UseWorkspaceManagementProps) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const queryClient = useQueryClient()
|
||||
const switchToWorkspace = useWorkflowRegistry((state) => state.switchToWorkspace)
|
||||
|
||||
// Workspace management state
|
||||
const [workspaces, setWorkspaces] = useState<Workspace[]>([])
|
||||
const [activeWorkspace, setActiveWorkspace] = useState<Workspace | null>(null)
|
||||
const [isWorkspacesLoading, setIsWorkspacesLoading] = useState(true)
|
||||
const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [isLeaving, setIsLeaving] = useState(false)
|
||||
const {
|
||||
data: workspaces = [],
|
||||
isLoading: isWorkspacesLoading,
|
||||
refetch: refetchWorkspaces,
|
||||
} = useWorkspacesQuery(Boolean(sessionUserId))
|
||||
|
||||
const leaveWorkspaceMutation = useLeaveWorkspace()
|
||||
const createWorkspaceMutation = useCreateWorkspace()
|
||||
const deleteWorkspaceMutation = useDeleteWorkspace()
|
||||
const updateWorkspaceNameMutation = useUpdateWorkspaceName()
|
||||
|
||||
// Refs to avoid dependency issues
|
||||
const workspaceIdRef = useRef<string>(workspaceId)
|
||||
const routerRef = useRef<ReturnType<typeof useRouter>>(router)
|
||||
const pathnameRef = useRef<string | null>(pathname || null)
|
||||
const activeWorkspaceRef = useRef<Workspace | null>(null)
|
||||
const isInitializedRef = useRef<boolean>(false)
|
||||
const hasValidatedRef = useRef<boolean>(false)
|
||||
|
||||
// Update refs when values change
|
||||
workspaceIdRef.current = workspaceId
|
||||
routerRef.current = router
|
||||
pathnameRef.current = pathname || null
|
||||
|
||||
const activeWorkspace = useMemo(() => {
|
||||
if (!workspaces.length) return null
|
||||
return workspaces.find((w) => w.id === workspaceId) ?? null
|
||||
}, [workspaces, workspaceId])
|
||||
|
||||
const activeWorkspaceRef = useRef<Workspace | null>(activeWorkspace)
|
||||
activeWorkspaceRef.current = activeWorkspace
|
||||
|
||||
/**
|
||||
* Refresh workspace list without validation logic - used for non-current workspace operations
|
||||
*/
|
||||
const refreshWorkspaceList = useCallback(async () => {
|
||||
setIsWorkspacesLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/workspaces')
|
||||
const data = await response.json()
|
||||
|
||||
if (data.workspaces && Array.isArray(data.workspaces)) {
|
||||
const fetchedWorkspaces = data.workspaces as Workspace[]
|
||||
setWorkspaces(fetchedWorkspaces)
|
||||
|
||||
// Only update activeWorkspace if it still exists in the fetched workspaces
|
||||
// Use functional update to avoid dependency on activeWorkspace
|
||||
setActiveWorkspace((currentActive) => {
|
||||
if (!currentActive) {
|
||||
return currentActive
|
||||
}
|
||||
|
||||
const matchingWorkspace = fetchedWorkspaces.find(
|
||||
(workspace) => workspace.id === currentActive.id
|
||||
)
|
||||
if (matchingWorkspace) {
|
||||
return matchingWorkspace
|
||||
}
|
||||
|
||||
// Active workspace was deleted, clear it
|
||||
logger.warn(`Active workspace ${currentActive.id} no longer exists`)
|
||||
return null
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Error refreshing workspace list:', err)
|
||||
} finally {
|
||||
setIsWorkspacesLoading(false)
|
||||
useEffect(() => {
|
||||
if (isWorkspacesLoading || hasValidatedRef.current || !workspaces.length) {
|
||||
return
|
||||
}
|
||||
}, [])
|
||||
|
||||
const currentWorkspaceId = workspaceIdRef.current
|
||||
const matchingWorkspace = workspaces.find((w) => w.id === currentWorkspaceId)
|
||||
|
||||
if (!matchingWorkspace) {
|
||||
logger.warn(`Workspace ${currentWorkspaceId} not found in user's workspaces`)
|
||||
const fallbackWorkspace = workspaces[0]
|
||||
logger.info(`Redirecting to fallback workspace: ${fallbackWorkspace.id}`)
|
||||
routerRef.current?.push(`/workspace/${fallbackWorkspace.id}/w`)
|
||||
}
|
||||
|
||||
hasValidatedRef.current = true
|
||||
}, [workspaces, isWorkspacesLoading])
|
||||
|
||||
const refreshWorkspaceList = useCallback(async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: workspaceKeys.lists() })
|
||||
}, [queryClient])
|
||||
|
||||
const fetchWorkspaces = useCallback(async () => {
|
||||
setIsWorkspacesLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/workspaces')
|
||||
const data = await response.json()
|
||||
hasValidatedRef.current = false
|
||||
await refetchWorkspaces()
|
||||
}, [refetchWorkspaces])
|
||||
|
||||
if (data.workspaces && Array.isArray(data.workspaces)) {
|
||||
const fetchedWorkspaces = data.workspaces as Workspace[]
|
||||
setWorkspaces(fetchedWorkspaces)
|
||||
|
||||
// Handle active workspace selection with URL validation using refs
|
||||
const currentWorkspaceId = workspaceIdRef.current
|
||||
const currentRouter = routerRef.current
|
||||
|
||||
if (currentWorkspaceId) {
|
||||
const matchingWorkspace = fetchedWorkspaces.find(
|
||||
(workspace) => workspace.id === currentWorkspaceId
|
||||
)
|
||||
if (matchingWorkspace) {
|
||||
setActiveWorkspace(matchingWorkspace)
|
||||
} else {
|
||||
logger.warn(`Workspace ${currentWorkspaceId} not found in user's workspaces`)
|
||||
|
||||
// Fallback to first workspace if current not found
|
||||
if (fetchedWorkspaces.length > 0) {
|
||||
const fallbackWorkspace = fetchedWorkspaces[0]
|
||||
setActiveWorkspace(fallbackWorkspace)
|
||||
|
||||
// Update URL to match the fallback workspace
|
||||
logger.info(`Redirecting to fallback workspace: ${fallbackWorkspace.id}`)
|
||||
currentRouter?.push(`/workspace/${fallbackWorkspace.id}/w`)
|
||||
} else {
|
||||
logger.error('No workspaces available for user')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Error fetching workspaces:', err)
|
||||
} finally {
|
||||
setIsWorkspacesLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Update workspace name both in API and local state
|
||||
*/
|
||||
const updateWorkspaceName = useCallback(
|
||||
async (workspaceId: string, newName: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`/api/workspaces/${workspaceId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: newName.trim() }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to update workspace name')
|
||||
}
|
||||
|
||||
// Update local state immediately after successful API call
|
||||
// Only update activeWorkspace if it's the one being renamed
|
||||
setActiveWorkspace((prev) =>
|
||||
prev && prev.id === workspaceId ? { ...prev, name: newName.trim() } : prev
|
||||
)
|
||||
setWorkspaces((prev) =>
|
||||
prev.map((workspace) =>
|
||||
workspace.id === workspaceId ? { ...workspace, name: newName.trim() } : workspace
|
||||
)
|
||||
)
|
||||
|
||||
await updateWorkspaceNameMutation.mutateAsync({ workspaceId, name: newName })
|
||||
logger.info('Successfully updated workspace name to:', newName.trim())
|
||||
return true
|
||||
} catch (error) {
|
||||
@@ -174,21 +104,18 @@ export function useWorkspaceManagement({
|
||||
return false
|
||||
}
|
||||
},
|
||||
[]
|
||||
[updateWorkspaceNameMutation]
|
||||
)
|
||||
|
||||
const switchWorkspace = useCallback(
|
||||
async (workspace: Workspace) => {
|
||||
// If already on this workspace, return
|
||||
if (activeWorkspaceRef.current?.id === workspace.id) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Switch workspace and update URL
|
||||
await switchToWorkspace(workspace.id)
|
||||
const currentPath = pathnameRef.current || ''
|
||||
// Preserve templates route if user is on templates or template detail
|
||||
const templateDetailMatch = currentPath.match(/^\/workspace\/[^/]+\/templates\/([^/]+)$/)
|
||||
if (templateDetailMatch) {
|
||||
const templateId = templateDetailMatch[1]
|
||||
@@ -206,208 +133,122 @@ export function useWorkspaceManagement({
|
||||
[switchToWorkspace]
|
||||
)
|
||||
|
||||
/**
|
||||
* Handle create workspace
|
||||
*/
|
||||
const handleCreateWorkspace = useCallback(async () => {
|
||||
if (isCreatingWorkspace) {
|
||||
if (createWorkspaceMutation.isPending) {
|
||||
logger.info('Workspace creation already in progress, ignoring request')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsCreatingWorkspace(true)
|
||||
logger.info('Creating new workspace')
|
||||
|
||||
// Generate workspace name using utility function
|
||||
const workspaceName = await generateWorkspaceName()
|
||||
|
||||
logger.info(`Generated workspace name: ${workspaceName}`)
|
||||
|
||||
const response = await fetch('/api/workspaces', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: workspaceName,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to create workspace')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const newWorkspace = data.workspace
|
||||
|
||||
const newWorkspace = await createWorkspaceMutation.mutateAsync({ name: workspaceName })
|
||||
logger.info('Created new workspace:', newWorkspace)
|
||||
|
||||
// Refresh workspace list (no URL validation needed for creation)
|
||||
await refreshWorkspaceList()
|
||||
|
||||
// Switch to the new workspace
|
||||
await switchWorkspace(newWorkspace)
|
||||
} catch (error) {
|
||||
logger.error('Error creating workspace:', error)
|
||||
} finally {
|
||||
setIsCreatingWorkspace(false)
|
||||
}
|
||||
}, [refreshWorkspaceList, switchWorkspace, isCreatingWorkspace])
|
||||
}, [createWorkspaceMutation, switchWorkspace])
|
||||
|
||||
/**
|
||||
* Confirm delete workspace
|
||||
*/
|
||||
const confirmDeleteWorkspace = useCallback(
|
||||
async (workspaceToDelete: Workspace, templateAction?: 'keep' | 'delete') => {
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
logger.info('Deleting workspace:', workspaceToDelete.id)
|
||||
|
||||
const deleteTemplates = templateAction === 'delete'
|
||||
|
||||
const response = await fetch(`/api/workspaces/${workspaceToDelete.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ deleteTemplates }),
|
||||
await deleteWorkspaceMutation.mutateAsync({
|
||||
workspaceId: workspaceToDelete.id,
|
||||
deleteTemplates,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to delete workspace')
|
||||
}
|
||||
|
||||
logger.info('Workspace deleted successfully:', workspaceToDelete.id)
|
||||
|
||||
// Check if we're deleting the current workspace (either active or in URL)
|
||||
const isDeletingCurrentWorkspace =
|
||||
workspaceIdRef.current === workspaceToDelete.id ||
|
||||
activeWorkspaceRef.current?.id === workspaceToDelete.id
|
||||
|
||||
if (isDeletingCurrentWorkspace) {
|
||||
// For current workspace deletion, use full fetchWorkspaces with URL validation
|
||||
logger.info(
|
||||
'Deleting current workspace - using full workspace refresh with URL validation'
|
||||
)
|
||||
await fetchWorkspaces()
|
||||
hasValidatedRef.current = false
|
||||
const { data: updatedWorkspaces } = await refetchWorkspaces()
|
||||
|
||||
// If we deleted the active workspace, switch to the first available workspace
|
||||
if (activeWorkspaceRef.current?.id === workspaceToDelete.id) {
|
||||
const remainingWorkspaces = workspaces.filter((w) => w.id !== workspaceToDelete.id)
|
||||
if (remainingWorkspaces.length > 0) {
|
||||
await switchWorkspace(remainingWorkspaces[0])
|
||||
}
|
||||
const remainingWorkspaces = (updatedWorkspaces || []).filter(
|
||||
(w) => w.id !== workspaceToDelete.id
|
||||
)
|
||||
if (remainingWorkspaces.length > 0) {
|
||||
await switchWorkspace(remainingWorkspaces[0])
|
||||
}
|
||||
} else {
|
||||
// For non-current workspace deletion, just refresh the list without URL validation
|
||||
logger.info('Deleting non-current workspace - using simple list refresh')
|
||||
await refreshWorkspaceList()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error deleting workspace:', error)
|
||||
} finally {
|
||||
setIsDeleting(false)
|
||||
}
|
||||
},
|
||||
[fetchWorkspaces, refreshWorkspaceList, workspaces, switchWorkspace]
|
||||
[deleteWorkspaceMutation, refetchWorkspaces, switchWorkspace]
|
||||
)
|
||||
|
||||
/**
|
||||
* Handle leave workspace
|
||||
*/
|
||||
const handleLeaveWorkspace = useCallback(
|
||||
async (workspaceToLeave: Workspace) => {
|
||||
setIsLeaving(true)
|
||||
if (!sessionUserId) {
|
||||
logger.error('Cannot leave workspace: no session user ID')
|
||||
return
|
||||
}
|
||||
|
||||
logger.info('Leaving workspace:', workspaceToLeave.id)
|
||||
|
||||
try {
|
||||
logger.info('Leaving workspace:', workspaceToLeave.id)
|
||||
|
||||
// Use the existing member removal API with current user's ID
|
||||
const response = await fetch(`/api/workspaces/members/${sessionUserId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
workspaceId: workspaceToLeave.id,
|
||||
}),
|
||||
await leaveWorkspaceMutation.mutateAsync({
|
||||
userId: sessionUserId,
|
||||
workspaceId: workspaceToLeave.id,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to leave workspace')
|
||||
}
|
||||
|
||||
logger.info('Left workspace successfully:', workspaceToLeave.id)
|
||||
|
||||
// Check if we're leaving the current workspace (either active or in URL)
|
||||
const isLeavingCurrentWorkspace =
|
||||
workspaceIdRef.current === workspaceToLeave.id ||
|
||||
activeWorkspaceRef.current?.id === workspaceToLeave.id
|
||||
|
||||
if (isLeavingCurrentWorkspace) {
|
||||
// For current workspace leaving, use full fetchWorkspaces with URL validation
|
||||
logger.info(
|
||||
'Leaving current workspace - using full workspace refresh with URL validation'
|
||||
)
|
||||
await fetchWorkspaces()
|
||||
hasValidatedRef.current = false
|
||||
const { data: updatedWorkspaces } = await refetchWorkspaces()
|
||||
|
||||
// If we left the active workspace, switch to the first available workspace
|
||||
if (activeWorkspaceRef.current?.id === workspaceToLeave.id) {
|
||||
const remainingWorkspaces = workspaces.filter((w) => w.id !== workspaceToLeave.id)
|
||||
if (remainingWorkspaces.length > 0) {
|
||||
await switchWorkspace(remainingWorkspaces[0])
|
||||
}
|
||||
const remainingWorkspaces = (updatedWorkspaces || []).filter(
|
||||
(w) => w.id !== workspaceToLeave.id
|
||||
)
|
||||
if (remainingWorkspaces.length > 0) {
|
||||
await switchWorkspace(remainingWorkspaces[0])
|
||||
}
|
||||
} else {
|
||||
// For non-current workspace leaving, just refresh the list without URL validation
|
||||
logger.info('Leaving non-current workspace - using simple list refresh')
|
||||
await refreshWorkspaceList()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error leaving workspace:', error)
|
||||
} finally {
|
||||
setIsLeaving(false)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
[fetchWorkspaces, refreshWorkspaceList, workspaces, switchWorkspace, sessionUserId]
|
||||
[refetchWorkspaces, switchWorkspace, sessionUserId, leaveWorkspaceMutation]
|
||||
)
|
||||
|
||||
/**
|
||||
* Validate workspace exists before making API calls
|
||||
*/
|
||||
const isWorkspaceValid = useCallback(async (workspaceId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/workspaces/${workspaceId}`)
|
||||
return response.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Initialize workspace data on mount (uses full validation with URL handling)
|
||||
* fetchWorkspaces is stable (empty deps array), so it's safe to call without including it
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (sessionUserId && !isInitializedRef.current) {
|
||||
isInitializedRef.current = true
|
||||
fetchWorkspaces()
|
||||
}
|
||||
}, [sessionUserId, fetchWorkspaces])
|
||||
const isWorkspaceValid = useCallback(
|
||||
(targetWorkspaceId: string) => {
|
||||
return workspaces.some((w) => w.id === targetWorkspaceId)
|
||||
},
|
||||
[workspaces]
|
||||
)
|
||||
|
||||
return {
|
||||
// State
|
||||
workspaces,
|
||||
activeWorkspace,
|
||||
isWorkspacesLoading,
|
||||
isCreatingWorkspace,
|
||||
isDeleting,
|
||||
isLeaving,
|
||||
|
||||
// Operations
|
||||
isCreatingWorkspace: createWorkspaceMutation.isPending,
|
||||
isDeleting: deleteWorkspaceMutation.isPending,
|
||||
isLeaving: leaveWorkspaceMutation.isPending,
|
||||
fetchWorkspaces,
|
||||
refreshWorkspaceList,
|
||||
updateWorkspaceName,
|
||||
@@ -418,3 +259,5 @@ export function useWorkspaceManagement({
|
||||
isWorkspaceValid,
|
||||
}
|
||||
}
|
||||
|
||||
export type { Workspace }
|
||||
|
||||
@@ -322,7 +322,8 @@ describe('ConditionBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
expect(mockCollectBlockData).toHaveBeenCalledWith(mockContext)
|
||||
// collectBlockData is now called with the current node ID for parallel branch context
|
||||
expect(mockCollectBlockData).toHaveBeenCalledWith(mockContext, mockBlock.id)
|
||||
})
|
||||
|
||||
it('should handle function_execute tool failure', async () => {
|
||||
@@ -620,4 +621,248 @@ describe('ConditionBlockHandler', () => {
|
||||
expect(mockContext.decisions.condition.has(mockBlock.id)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Parallel branch handling', () => {
|
||||
it('should resolve connections and block data correctly when inside a parallel branch', async () => {
|
||||
// Simulate a condition block inside a parallel branch
|
||||
// Virtual block ID uses subscript notation: blockId₍branchIndex₎
|
||||
const parallelConditionBlock: SerializedBlock = {
|
||||
id: 'cond-block-1₍0₎', // Virtual ID for branch 0
|
||||
metadata: { id: 'condition', name: 'Condition' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: {},
|
||||
}
|
||||
|
||||
// Source block also has a virtual ID in the same branch
|
||||
const sourceBlockVirtualId = 'agent-block-1₍0₎'
|
||||
|
||||
// Set up workflow with connections using BASE block IDs (as they are in the workflow definition)
|
||||
const parallelWorkflow: SerializedWorkflow = {
|
||||
blocks: [
|
||||
{
|
||||
id: 'agent-block-1',
|
||||
metadata: { id: 'agent', name: 'Agent' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: 'cond-block-1',
|
||||
metadata: { id: 'condition', name: 'Condition' },
|
||||
position: { x: 100, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: 'target-block-1',
|
||||
metadata: { id: 'api', name: 'Target' },
|
||||
position: { x: 200, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
],
|
||||
connections: [
|
||||
// Connections use base IDs, not virtual IDs
|
||||
{ source: 'agent-block-1', target: 'cond-block-1' },
|
||||
{ source: 'cond-block-1', target: 'target-block-1', sourceHandle: 'condition-cond1' },
|
||||
],
|
||||
loops: [],
|
||||
parallels: [],
|
||||
}
|
||||
|
||||
// Block states use virtual IDs (as outputs are stored per-branch)
|
||||
const parallelBlockStates = new Map<string, BlockState>([
|
||||
[
|
||||
sourceBlockVirtualId,
|
||||
{ output: { response: 'hello from branch 0', success: true }, executed: true },
|
||||
],
|
||||
])
|
||||
|
||||
const parallelContext: ExecutionContext = {
|
||||
workflowId: 'test-workflow-id',
|
||||
workspaceId: 'test-workspace-id',
|
||||
workflow: parallelWorkflow,
|
||||
blockStates: parallelBlockStates,
|
||||
blockLogs: [],
|
||||
completedBlocks: new Set(),
|
||||
decisions: {
|
||||
router: new Map(),
|
||||
condition: new Map(),
|
||||
},
|
||||
environmentVariables: {},
|
||||
workflowVariables: {},
|
||||
}
|
||||
|
||||
const conditions = [
|
||||
{ id: 'cond1', title: 'if', value: 'context.response === "hello from branch 0"' },
|
||||
{ id: 'else1', title: 'else', value: '' },
|
||||
]
|
||||
const inputs = { conditions: JSON.stringify(conditions) }
|
||||
|
||||
const result = await handler.execute(parallelContext, parallelConditionBlock, inputs)
|
||||
|
||||
// The condition should evaluate to true because:
|
||||
// 1. Connection lookup uses base ID 'cond-block-1' (extracted from 'cond-block-1₍0₎')
|
||||
// 2. Source block output is found at virtual ID 'agent-block-1₍0₎' (same branch)
|
||||
// 3. The evaluation context contains { response: 'hello from branch 0' }
|
||||
expect((result as any).conditionResult).toBe(true)
|
||||
expect((result as any).selectedOption).toBe('cond1')
|
||||
expect((result as any).selectedPath).toEqual({
|
||||
blockId: 'target-block-1',
|
||||
blockType: 'api',
|
||||
blockTitle: 'Target',
|
||||
})
|
||||
})
|
||||
|
||||
it('should find correct source block output in parallel branch context', async () => {
|
||||
// Test that when multiple branches exist, the correct branch output is used
|
||||
const parallelConditionBlock: SerializedBlock = {
|
||||
id: 'cond-block-1₍1₎', // Virtual ID for branch 1
|
||||
metadata: { id: 'condition', name: 'Condition' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: {},
|
||||
}
|
||||
|
||||
const parallelWorkflow: SerializedWorkflow = {
|
||||
blocks: [
|
||||
{
|
||||
id: 'agent-block-1',
|
||||
metadata: { id: 'agent', name: 'Agent' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: 'cond-block-1',
|
||||
metadata: { id: 'condition', name: 'Condition' },
|
||||
position: { x: 100, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: 'target-block-1',
|
||||
metadata: { id: 'api', name: 'Target' },
|
||||
position: { x: 200, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
],
|
||||
connections: [
|
||||
{ source: 'agent-block-1', target: 'cond-block-1' },
|
||||
{ source: 'cond-block-1', target: 'target-block-1', sourceHandle: 'condition-cond1' },
|
||||
],
|
||||
loops: [],
|
||||
parallels: [],
|
||||
}
|
||||
|
||||
// Multiple branches have executed - each has different output
|
||||
const parallelBlockStates = new Map<string, BlockState>([
|
||||
['agent-block-1₍0₎', { output: { value: 10 }, executed: true }],
|
||||
['agent-block-1₍1₎', { output: { value: 25 }, executed: true }], // Branch 1 has value 25
|
||||
['agent-block-1₍2₎', { output: { value: 5 }, executed: true }],
|
||||
])
|
||||
|
||||
const parallelContext: ExecutionContext = {
|
||||
workflowId: 'test-workflow-id',
|
||||
workspaceId: 'test-workspace-id',
|
||||
workflow: parallelWorkflow,
|
||||
blockStates: parallelBlockStates,
|
||||
blockLogs: [],
|
||||
completedBlocks: new Set(),
|
||||
decisions: {
|
||||
router: new Map(),
|
||||
condition: new Map(),
|
||||
},
|
||||
environmentVariables: {},
|
||||
workflowVariables: {},
|
||||
}
|
||||
|
||||
// Condition checks if value > 20 - should be true for branch 1 (value=25)
|
||||
const conditions = [
|
||||
{ id: 'cond1', title: 'if', value: 'context.value > 20' },
|
||||
{ id: 'else1', title: 'else', value: '' },
|
||||
]
|
||||
const inputs = { conditions: JSON.stringify(conditions) }
|
||||
|
||||
const result = await handler.execute(parallelContext, parallelConditionBlock, inputs)
|
||||
|
||||
// Should evaluate using branch 1's data (value=25), not branch 0 (value=10) or branch 2 (value=5)
|
||||
expect((result as any).conditionResult).toBe(true)
|
||||
expect((result as any).selectedOption).toBe('cond1')
|
||||
})
|
||||
|
||||
it('should fall back to else when condition is false in parallel branch', async () => {
|
||||
const parallelConditionBlock: SerializedBlock = {
|
||||
id: 'cond-block-1₍2₎', // Virtual ID for branch 2
|
||||
metadata: { id: 'condition', name: 'Condition' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: {},
|
||||
}
|
||||
|
||||
const parallelWorkflow: SerializedWorkflow = {
|
||||
blocks: [
|
||||
{
|
||||
id: 'agent-block-1',
|
||||
metadata: { id: 'agent', name: 'Agent' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: 'cond-block-1',
|
||||
metadata: { id: 'condition', name: 'Condition' },
|
||||
position: { x: 100, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: 'target-true',
|
||||
metadata: { id: 'api', name: 'True Path' },
|
||||
position: { x: 200, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: 'target-false',
|
||||
metadata: { id: 'api', name: 'False Path' },
|
||||
position: { x: 200, y: 100 },
|
||||
config: {},
|
||||
},
|
||||
],
|
||||
connections: [
|
||||
{ source: 'agent-block-1', target: 'cond-block-1' },
|
||||
{ source: 'cond-block-1', target: 'target-true', sourceHandle: 'condition-cond1' },
|
||||
{ source: 'cond-block-1', target: 'target-false', sourceHandle: 'condition-else1' },
|
||||
],
|
||||
loops: [],
|
||||
parallels: [],
|
||||
}
|
||||
|
||||
const parallelBlockStates = new Map<string, BlockState>([
|
||||
['agent-block-1₍0₎', { output: { value: 100 }, executed: true }],
|
||||
['agent-block-1₍1₎', { output: { value: 50 }, executed: true }],
|
||||
['agent-block-1₍2₎', { output: { value: 5 }, executed: true }], // Branch 2 has value 5
|
||||
])
|
||||
|
||||
const parallelContext: ExecutionContext = {
|
||||
workflowId: 'test-workflow-id',
|
||||
workspaceId: 'test-workspace-id',
|
||||
workflow: parallelWorkflow,
|
||||
blockStates: parallelBlockStates,
|
||||
blockLogs: [],
|
||||
completedBlocks: new Set(),
|
||||
decisions: {
|
||||
router: new Map(),
|
||||
condition: new Map(),
|
||||
},
|
||||
environmentVariables: {},
|
||||
workflowVariables: {},
|
||||
}
|
||||
|
||||
// Condition checks if value > 20 - should be false for branch 2 (value=5)
|
||||
const conditions = [
|
||||
{ id: 'cond1', title: 'if', value: 'context.value > 20' },
|
||||
{ id: 'else1', title: 'else', value: '' },
|
||||
]
|
||||
const inputs = { conditions: JSON.stringify(conditions) }
|
||||
|
||||
const result = await handler.execute(parallelContext, parallelConditionBlock, inputs)
|
||||
|
||||
// Should fall back to else path because branch 2's value (5) is not > 20
|
||||
expect((result as any).conditionResult).toBe(true)
|
||||
expect((result as any).selectedOption).toBe('else1')
|
||||
expect((result as any).selectedPath.blockId).toBe('target-false')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,12 @@ import type { BlockOutput } from '@/blocks/types'
|
||||
import { BlockType, CONDITION, DEFAULTS, EDGE } from '@/executor/constants'
|
||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||
import { collectBlockData } from '@/executor/utils/block-data'
|
||||
import {
|
||||
buildBranchNodeId,
|
||||
extractBaseBlockId,
|
||||
extractBranchIndex,
|
||||
isBranchNodeId,
|
||||
} from '@/executor/utils/subflow-utils'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import { executeTool } from '@/tools'
|
||||
|
||||
@@ -18,7 +24,8 @@ const CONDITION_TIMEOUT_MS = 5000
|
||||
export async function evaluateConditionExpression(
|
||||
ctx: ExecutionContext,
|
||||
conditionExpression: string,
|
||||
providedEvalContext?: Record<string, any>
|
||||
providedEvalContext?: Record<string, any>,
|
||||
currentNodeId?: string
|
||||
): Promise<boolean> {
|
||||
const evalContext = providedEvalContext || {}
|
||||
|
||||
@@ -26,7 +33,7 @@ export async function evaluateConditionExpression(
|
||||
const contextSetup = `const context = ${JSON.stringify(evalContext)};`
|
||||
const code = `${contextSetup}\nreturn Boolean(${conditionExpression})`
|
||||
|
||||
const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx)
|
||||
const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx, currentNodeId)
|
||||
|
||||
const result = await executeTool(
|
||||
'function_execute',
|
||||
@@ -83,7 +90,19 @@ export class ConditionBlockHandler implements BlockHandler {
|
||||
): Promise<BlockOutput> {
|
||||
const conditions = this.parseConditions(inputs.conditions)
|
||||
|
||||
const sourceBlockId = ctx.workflow?.connections.find((conn) => conn.target === block.id)?.source
|
||||
const baseBlockId = extractBaseBlockId(block.id)
|
||||
const branchIndex = isBranchNodeId(block.id) ? extractBranchIndex(block.id) : null
|
||||
|
||||
const sourceConnection = ctx.workflow?.connections.find((conn) => conn.target === baseBlockId)
|
||||
let sourceBlockId = sourceConnection?.source
|
||||
|
||||
if (sourceBlockId && branchIndex !== null) {
|
||||
const virtualSourceId = buildBranchNodeId(sourceBlockId, branchIndex)
|
||||
if (ctx.blockStates.has(virtualSourceId)) {
|
||||
sourceBlockId = virtualSourceId
|
||||
}
|
||||
}
|
||||
|
||||
const evalContext = this.buildEvaluationContext(ctx, sourceBlockId)
|
||||
const rawSourceOutput = sourceBlockId ? ctx.blockStates.get(sourceBlockId)?.output : null
|
||||
|
||||
@@ -91,13 +110,16 @@ export class ConditionBlockHandler implements BlockHandler {
|
||||
// thinking this block is pausing (it was already resumed by the HITL block)
|
||||
const sourceOutput = this.filterPauseMetadata(rawSourceOutput)
|
||||
|
||||
const outgoingConnections = ctx.workflow?.connections.filter((conn) => conn.source === block.id)
|
||||
const outgoingConnections = ctx.workflow?.connections.filter(
|
||||
(conn) => conn.source === baseBlockId
|
||||
)
|
||||
|
||||
const { selectedConnection, selectedCondition } = await this.evaluateConditions(
|
||||
conditions,
|
||||
outgoingConnections || [],
|
||||
evalContext,
|
||||
ctx
|
||||
ctx,
|
||||
block.id
|
||||
)
|
||||
|
||||
if (!selectedConnection || !selectedCondition) {
|
||||
@@ -170,7 +192,8 @@ export class ConditionBlockHandler implements BlockHandler {
|
||||
conditions: Array<{ id: string; title: string; value: string }>,
|
||||
outgoingConnections: Array<{ source: string; target: string; sourceHandle?: string }>,
|
||||
evalContext: Record<string, any>,
|
||||
ctx: ExecutionContext
|
||||
ctx: ExecutionContext,
|
||||
currentNodeId?: string
|
||||
): Promise<{
|
||||
selectedConnection: { target: string; sourceHandle?: string } | null
|
||||
selectedCondition: { id: string; title: string; value: string } | null
|
||||
@@ -189,7 +212,8 @@ export class ConditionBlockHandler implements BlockHandler {
|
||||
const conditionMet = await evaluateConditionExpression(
|
||||
ctx,
|
||||
conditionValueString,
|
||||
evalContext
|
||||
evalContext,
|
||||
currentNodeId
|
||||
)
|
||||
|
||||
if (conditionMet) {
|
||||
|
||||
@@ -2,6 +2,11 @@ import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
|
||||
import { isTriggerBehavior, normalizeName } from '@/executor/constants'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { OutputSchema } from '@/executor/utils/block-reference'
|
||||
import {
|
||||
extractBaseBlockId,
|
||||
extractBranchIndex,
|
||||
isBranchNodeId,
|
||||
} from '@/executor/utils/subflow-utils'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import { getTool } from '@/tools/utils'
|
||||
@@ -86,14 +91,30 @@ export function getBlockSchema(
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function collectBlockData(ctx: ExecutionContext): BlockDataCollection {
|
||||
export function collectBlockData(
|
||||
ctx: ExecutionContext,
|
||||
currentNodeId?: string
|
||||
): BlockDataCollection {
|
||||
const blockData: Record<string, unknown> = {}
|
||||
const blockNameMapping: Record<string, string> = {}
|
||||
const blockOutputSchemas: Record<string, OutputSchema> = {}
|
||||
|
||||
const branchIndex =
|
||||
currentNodeId && isBranchNodeId(currentNodeId) ? extractBranchIndex(currentNodeId) : null
|
||||
|
||||
for (const [id, state] of ctx.blockStates.entries()) {
|
||||
if (state.output !== undefined) {
|
||||
blockData[id] = state.output
|
||||
|
||||
if (branchIndex !== null && isBranchNodeId(id)) {
|
||||
const stateBranchIndex = extractBranchIndex(id)
|
||||
if (stateBranchIndex === branchIndex) {
|
||||
const baseId = extractBaseBlockId(id)
|
||||
if (blockData[baseId] === undefined) {
|
||||
blockData[baseId] = state.output
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
309
apps/sim/hooks/queries/invitations.ts
Normal file
309
apps/sim/hooks/queries/invitations.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { workspaceKeys } from './workspace'
|
||||
|
||||
/**
|
||||
* Query key factory for invitation-related queries.
|
||||
* Provides hierarchical cache keys for workspace invitations.
|
||||
*/
|
||||
export const invitationKeys = {
|
||||
all: ['invitations'] as const,
|
||||
lists: () => [...invitationKeys.all, 'list'] as const,
|
||||
list: (workspaceId: string) => [...invitationKeys.lists(), workspaceId] as const,
|
||||
}
|
||||
|
||||
/** Raw invitation data from the API. */
|
||||
export interface PendingInvitation {
|
||||
id: string
|
||||
workspaceId: string
|
||||
email: string
|
||||
permissions: 'admin' | 'write' | 'read'
|
||||
status: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
/** Normalized invitation for display in the UI. */
|
||||
export interface WorkspaceInvitation {
|
||||
email: string
|
||||
permissionType: 'admin' | 'write' | 'read'
|
||||
isPendingInvitation: boolean
|
||||
invitationId?: string
|
||||
}
|
||||
|
||||
async function fetchPendingInvitations(workspaceId: string): Promise<WorkspaceInvitation[]> {
|
||||
const response = await fetch('/api/workspaces/invitations')
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch pending invitations')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return (
|
||||
data.invitations
|
||||
?.filter(
|
||||
(inv: PendingInvitation) => inv.status === 'pending' && inv.workspaceId === workspaceId
|
||||
)
|
||||
.map((inv: PendingInvitation) => ({
|
||||
email: inv.email,
|
||||
permissionType: inv.permissions,
|
||||
isPendingInvitation: true,
|
||||
invitationId: inv.id,
|
||||
})) || []
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches pending invitations for a workspace.
|
||||
* @param workspaceId - The workspace ID to fetch invitations for
|
||||
*/
|
||||
export function usePendingInvitations(workspaceId: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: invitationKeys.list(workspaceId ?? ''),
|
||||
queryFn: () => fetchPendingInvitations(workspaceId as string),
|
||||
enabled: Boolean(workspaceId),
|
||||
staleTime: 30 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
interface BatchSendInvitationsParams {
|
||||
workspaceId: string
|
||||
invitations: Array<{ email: string; permission: 'admin' | 'write' | 'read' }>
|
||||
}
|
||||
|
||||
interface BatchInvitationResult {
|
||||
successful: string[]
|
||||
failed: Array<{ email: string; error: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends multiple workspace invitations in parallel.
|
||||
* Returns results for each invitation indicating success or failure.
|
||||
*/
|
||||
export function useBatchSendWorkspaceInvitations() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
workspaceId,
|
||||
invitations,
|
||||
}: BatchSendInvitationsParams): Promise<BatchInvitationResult> => {
|
||||
const results = await Promise.allSettled(
|
||||
invitations.map(async ({ email, permission }) => {
|
||||
const response = await fetch('/api/workspaces/invitations', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
workspaceId,
|
||||
email,
|
||||
role: 'member',
|
||||
permission,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to send invitation')
|
||||
}
|
||||
|
||||
return { email, data: await response.json() }
|
||||
})
|
||||
)
|
||||
|
||||
const successful: string[] = []
|
||||
const failed: Array<{ email: string; error: string }> = []
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const email = invitations[index].email
|
||||
if (result.status === 'fulfilled') {
|
||||
successful.push(email)
|
||||
} else {
|
||||
failed.push({ email, error: result.reason?.message || 'Unknown error' })
|
||||
}
|
||||
})
|
||||
|
||||
return { successful, failed }
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: invitationKeys.list(variables.workspaceId),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface CancelInvitationParams {
|
||||
invitationId: string
|
||||
workspaceId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a pending workspace invitation.
|
||||
* Invalidates the invitation list cache on success.
|
||||
*/
|
||||
export function useCancelWorkspaceInvitation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ invitationId }: CancelInvitationParams) => {
|
||||
const response = await fetch(`/api/workspaces/invitations/${invitationId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to cancel invitation')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: invitationKeys.list(variables.workspaceId),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface ResendInvitationParams {
|
||||
invitationId: string
|
||||
workspaceId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Resends a pending workspace invitation email.
|
||||
* Invalidates the invitation list cache on success.
|
||||
*/
|
||||
export function useResendWorkspaceInvitation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ invitationId }: ResendInvitationParams) => {
|
||||
const response = await fetch(`/api/workspaces/invitations/${invitationId}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to resend invitation')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: invitationKeys.list(variables.workspaceId),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface RemoveMemberParams {
|
||||
userId: string
|
||||
workspaceId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a member from a workspace.
|
||||
* Invalidates the workspace permissions cache on success.
|
||||
*/
|
||||
export function useRemoveWorkspaceMember() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ userId, workspaceId }: RemoveMemberParams) => {
|
||||
const response = await fetch(`/api/workspaces/members/${userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ workspaceId }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to remove member')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.permissions(variables.workspaceId),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface LeaveWorkspaceParams {
|
||||
userId: string
|
||||
workspaceId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the current user to leave a workspace.
|
||||
* Invalidates both permissions and workspace list caches on success.
|
||||
*/
|
||||
export function useLeaveWorkspace() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ userId, workspaceId }: LeaveWorkspaceParams) => {
|
||||
const response = await fetch(`/api/workspaces/members/${userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ workspaceId }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to leave workspace')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.permissions(variables.workspaceId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.all,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface UpdatePermissionsParams {
|
||||
workspaceId: string
|
||||
updates: Array<{ userId: string; permissions: 'admin' | 'write' | 'read' }>
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates permissions for one or more workspace members.
|
||||
* Invalidates the workspace permissions cache on success.
|
||||
*/
|
||||
export function useUpdateWorkspacePermissions() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ workspaceId, updates }: UpdatePermissionsParams) => {
|
||||
const response = await fetch(`/api/workspaces/${workspaceId}/permissions`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ updates }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to update permissions')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.permissions(variables.workspaceId),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -6,27 +6,32 @@ import { OAUTH_PROVIDERS, type OAuthServiceConfig } from '@/lib/oauth'
|
||||
const logger = createLogger('OAuthConnectionsQuery')
|
||||
|
||||
/**
|
||||
* Query key factories for OAuth connections
|
||||
* Query key factory for OAuth connection queries.
|
||||
* Provides hierarchical cache keys for connections and provider-specific accounts.
|
||||
*/
|
||||
export const oauthConnectionsKeys = {
|
||||
all: ['oauthConnections'] as const,
|
||||
connections: () => [...oauthConnectionsKeys.all, 'connections'] as const,
|
||||
accounts: (provider: string) => [...oauthConnectionsKeys.all, 'accounts', provider] as const,
|
||||
}
|
||||
|
||||
/**
|
||||
* Service info type - extends OAuthServiceConfig with connection status and the service key
|
||||
*/
|
||||
/** OAuth service with connection status and linked accounts. */
|
||||
export interface ServiceInfo extends OAuthServiceConfig {
|
||||
/** The service key from OAUTH_PROVIDERS (e.g., 'gmail', 'google-drive') */
|
||||
id: string
|
||||
isConnected: boolean
|
||||
lastConnected?: string
|
||||
accounts?: { id: string; name: string }[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Define available services from standardized OAuth providers
|
||||
*/
|
||||
/** OAuth connection data returned from the API. */
|
||||
interface OAuthConnectionResponse {
|
||||
provider: string
|
||||
baseProvider?: string
|
||||
accounts?: { id: string; name: string }[]
|
||||
lastConnected?: string
|
||||
scopes?: string[]
|
||||
}
|
||||
|
||||
function defineServices(): ServiceInfo[] {
|
||||
const servicesList: ServiceInfo[] = []
|
||||
|
||||
@@ -44,9 +49,6 @@ function defineServices(): ServiceInfo[] {
|
||||
return servicesList
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch OAuth connections and merge with service definitions
|
||||
*/
|
||||
async function fetchOAuthConnections(): Promise<ServiceInfo[]> {
|
||||
try {
|
||||
const serviceDefinitions = defineServices()
|
||||
@@ -65,7 +67,9 @@ async function fetchOAuthConnections(): Promise<ServiceInfo[]> {
|
||||
const connections = data.connections || []
|
||||
|
||||
const updatedServices = serviceDefinitions.map((service) => {
|
||||
const connection = connections.find((conn: any) => conn.provider === service.providerId)
|
||||
const connection = connections.find(
|
||||
(conn: OAuthConnectionResponse) => conn.provider === service.providerId
|
||||
)
|
||||
|
||||
if (connection) {
|
||||
return {
|
||||
@@ -76,13 +80,14 @@ async function fetchOAuthConnections(): Promise<ServiceInfo[]> {
|
||||
}
|
||||
}
|
||||
|
||||
const connectionWithScopes = connections.find((conn: any) => {
|
||||
const connectionWithScopes = connections.find((conn: OAuthConnectionResponse) => {
|
||||
if (!conn.baseProvider || !service.providerId.startsWith(conn.baseProvider)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (conn.scopes && service.scopes) {
|
||||
return service.scopes.every((scope) => conn.scopes.includes(scope))
|
||||
const connScopes = conn.scopes
|
||||
return service.scopes.every((scope) => connScopes.includes(scope))
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -108,26 +113,28 @@ async function fetchOAuthConnections(): Promise<ServiceInfo[]> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch OAuth connections
|
||||
* Fetches all OAuth service connections with their status.
|
||||
* Returns service definitions merged with connection data.
|
||||
*/
|
||||
export function useOAuthConnections() {
|
||||
return useQuery({
|
||||
queryKey: oauthConnectionsKeys.connections(),
|
||||
queryFn: fetchOAuthConnections,
|
||||
staleTime: 30 * 1000, // 30 seconds - connections don't change often
|
||||
retry: false, // Don't retry on 404
|
||||
placeholderData: keepPreviousData, // Show cached data immediately
|
||||
staleTime: 30 * 1000,
|
||||
retry: false,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect OAuth service mutation
|
||||
*/
|
||||
interface ConnectServiceParams {
|
||||
providerId: string
|
||||
callbackURL: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates OAuth connection flow for a service.
|
||||
* Redirects the user to the provider's authorization page.
|
||||
*/
|
||||
export function useConnectOAuthService() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -138,7 +145,6 @@ export function useConnectOAuthService() {
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// Shopify requires a custom OAuth flow with shop domain input
|
||||
if (providerId === 'shopify') {
|
||||
const returnUrl = encodeURIComponent(callbackURL)
|
||||
window.location.href = `/api/auth/shopify/authorize?returnUrl=${returnUrl}`
|
||||
@@ -161,9 +167,6 @@ export function useConnectOAuthService() {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect OAuth service mutation
|
||||
*/
|
||||
interface DisconnectServiceParams {
|
||||
provider: string
|
||||
providerId: string
|
||||
@@ -171,6 +174,10 @@ interface DisconnectServiceParams {
|
||||
accountId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects an OAuth service account.
|
||||
* Performs optimistic update and rolls back on failure.
|
||||
*/
|
||||
export function useDisconnectOAuthService() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -230,3 +237,38 @@ export function useDisconnectOAuthService() {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** Connected OAuth account for a specific provider. */
|
||||
export interface ConnectedAccount {
|
||||
id: string
|
||||
accountId: string
|
||||
providerId: string
|
||||
displayName?: string
|
||||
}
|
||||
|
||||
async function fetchConnectedAccounts(provider: string): Promise<ConnectedAccount[]> {
|
||||
const response = await fetch(`/api/auth/accounts?provider=${provider}`)
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
throw new Error(data.error || `Failed to load ${provider} accounts`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.accounts || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches connected accounts for a specific OAuth provider.
|
||||
* @param provider - The provider ID (e.g., 'slack', 'google')
|
||||
* @param options - Query options including enabled flag
|
||||
*/
|
||||
export function useConnectedAccounts(provider: string, options?: { enabled?: boolean }) {
|
||||
return useQuery({
|
||||
queryKey: oauthConnectionsKeys.accounts(provider),
|
||||
queryFn: () => fetchConnectedAccounts(provider),
|
||||
enabled: options?.enabled ?? true,
|
||||
staleTime: 60 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
/**
|
||||
* Query key factories for workspace-related queries
|
||||
* Query key factory for workspace-related queries.
|
||||
* Provides hierarchical cache keys for workspaces, settings, and permissions.
|
||||
*/
|
||||
export const workspaceKeys = {
|
||||
all: ['workspace'] as const,
|
||||
lists: () => [...workspaceKeys.all, 'list'] as const,
|
||||
list: () => [...workspaceKeys.lists(), 'user'] as const,
|
||||
details: () => [...workspaceKeys.all, 'detail'] as const,
|
||||
detail: (id: string) => [...workspaceKeys.details(), id] as const,
|
||||
settings: (id: string) => [...workspaceKeys.detail(id), 'settings'] as const,
|
||||
@@ -13,9 +16,186 @@ export const workspaceKeys = {
|
||||
adminList: (userId: string | undefined) => [...workspaceKeys.adminLists(), userId ?? ''] as const,
|
||||
}
|
||||
|
||||
/** Represents a workspace in the user's workspace list. */
|
||||
export interface Workspace {
|
||||
id: string
|
||||
name: string
|
||||
ownerId: string
|
||||
role?: string
|
||||
membershipId?: string
|
||||
permissions?: 'admin' | 'write' | 'read' | null
|
||||
}
|
||||
|
||||
async function fetchWorkspaces(): Promise<Workspace[]> {
|
||||
const response = await fetch('/api/workspaces')
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch workspaces')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.workspaces || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch workspace settings
|
||||
* Fetches the current user's workspaces.
|
||||
* @param enabled - Whether the query should execute (defaults to true)
|
||||
*/
|
||||
export function useWorkspacesQuery(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.list(),
|
||||
queryFn: fetchWorkspaces,
|
||||
enabled,
|
||||
staleTime: 30 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
interface CreateWorkspaceParams {
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new workspace.
|
||||
* Automatically invalidates the workspace list cache on success.
|
||||
*/
|
||||
export function useCreateWorkspace() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ name }: CreateWorkspaceParams) => {
|
||||
const response = await fetch('/api/workspaces', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to create workspace')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.workspace as Workspace
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.lists() })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface DeleteWorkspaceParams {
|
||||
workspaceId: string
|
||||
deleteTemplates?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a workspace.
|
||||
* Automatically invalidates the workspace list cache on success.
|
||||
*/
|
||||
export function useDeleteWorkspace() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ workspaceId, deleteTemplates = false }: DeleteWorkspaceParams) => {
|
||||
const response = await fetch(`/api/workspaces/${workspaceId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ deleteTemplates }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to delete workspace')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.lists() })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface UpdateWorkspaceNameParams {
|
||||
workspaceId: string
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a workspace's name.
|
||||
* Invalidates both the workspace list and the specific workspace detail cache.
|
||||
*/
|
||||
export function useUpdateWorkspaceName() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ workspaceId, name }: UpdateWorkspaceNameParams) => {
|
||||
const response = await fetch(`/api/workspaces/${workspaceId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: name.trim() }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to update workspace name')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.lists() })
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.detail(variables.workspaceId) })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** Represents a user with permissions in a workspace. */
|
||||
export interface WorkspaceUser {
|
||||
userId: string
|
||||
email: string
|
||||
name: string | null
|
||||
image: string | null
|
||||
permissionType: 'admin' | 'write' | 'read'
|
||||
}
|
||||
|
||||
/** Workspace permissions data containing all users and their access levels. */
|
||||
export interface WorkspacePermissions {
|
||||
users: WorkspaceUser[]
|
||||
total: number
|
||||
}
|
||||
|
||||
async function fetchWorkspacePermissions(workspaceId: string): Promise<WorkspacePermissions> {
|
||||
const response = await fetch(`/api/workspaces/${workspaceId}/permissions`)
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Workspace not found or access denied')
|
||||
}
|
||||
if (response.status === 401) {
|
||||
throw new Error('Authentication required')
|
||||
}
|
||||
throw new Error(`Failed to fetch permissions: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches permissions for a specific workspace.
|
||||
* @param workspaceId - The workspace ID to fetch permissions for
|
||||
*/
|
||||
export function useWorkspacePermissionsQuery(workspaceId: string | null | undefined) {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.permissions(workspaceId ?? ''),
|
||||
queryFn: () => fetchWorkspacePermissions(workspaceId as string),
|
||||
enabled: Boolean(workspaceId),
|
||||
staleTime: 30 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchWorkspaceSettings(workspaceId: string) {
|
||||
const [settingsResponse, permissionsResponse] = await Promise.all([
|
||||
fetch(`/api/workspaces/${workspaceId}`),
|
||||
@@ -38,7 +218,8 @@ async function fetchWorkspaceSettings(workspaceId: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch workspace settings
|
||||
* Fetches workspace settings including permissions.
|
||||
* @param workspaceId - The workspace ID to fetch settings for
|
||||
*/
|
||||
export function useWorkspaceSettings(workspaceId: string) {
|
||||
return useQuery({
|
||||
@@ -50,15 +231,16 @@ export function useWorkspaceSettings(workspaceId: string) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update workspace settings mutation
|
||||
*/
|
||||
interface UpdateWorkspaceSettingsParams {
|
||||
workspaceId: string
|
||||
billedAccountUserId?: string
|
||||
billingAccountUserEmail?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates workspace settings (e.g., billing configuration).
|
||||
* Invalidates the workspace settings cache on success.
|
||||
*/
|
||||
export function useUpdateWorkspaceSettings() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -85,9 +267,7 @@ export function useUpdateWorkspaceSettings() {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Workspace type returned by admin workspaces query
|
||||
*/
|
||||
/** Workspace with admin access metadata. */
|
||||
export interface AdminWorkspace {
|
||||
id: string
|
||||
name: string
|
||||
@@ -96,9 +276,6 @@ export interface AdminWorkspace {
|
||||
canInvite: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch workspaces where user has admin access
|
||||
*/
|
||||
async function fetchAdminWorkspaces(userId: string | undefined): Promise<AdminWorkspace[]> {
|
||||
if (!userId) {
|
||||
return []
|
||||
@@ -121,7 +298,7 @@ async function fetchAdminWorkspaces(userId: string | undefined): Promise<AdminWo
|
||||
}
|
||||
const permissionData = await permissionResponse.json()
|
||||
return { workspace, permissionData }
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -161,14 +338,15 @@ async function fetchAdminWorkspaces(userId: string | undefined): Promise<AdminWo
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch workspaces where user has admin access
|
||||
* Fetches workspaces where the user has admin access.
|
||||
* @param userId - The user ID to check admin access for
|
||||
*/
|
||||
export function useAdminWorkspaces(userId: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.adminList(userId),
|
||||
queryFn: () => fetchAdminWorkspaces(userId),
|
||||
enabled: Boolean(userId),
|
||||
staleTime: 60 * 1000, // Cache for 60 seconds
|
||||
staleTime: 60 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
interface SlackAccount {
|
||||
id: string
|
||||
accountId: string
|
||||
providerId: string
|
||||
displayName?: string
|
||||
}
|
||||
|
||||
interface UseSlackAccountsResult {
|
||||
accounts: SlackAccount[]
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
refetch: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and manages connected Slack accounts for the current user.
|
||||
* @returns Object containing accounts array, loading state, error state, and refetch function
|
||||
*/
|
||||
export function useSlackAccounts(): UseSlackAccountsResult {
|
||||
const [accounts, setAccounts] = useState<SlackAccount[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchAccounts = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
const response = await fetch('/api/auth/accounts?provider=slack')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setAccounts(data.accounts || [])
|
||||
} else {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
setError(data.error || 'Failed to load Slack accounts')
|
||||
setAccounts([])
|
||||
}
|
||||
} catch {
|
||||
setError('Failed to load Slack accounts')
|
||||
setAccounts([])
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts()
|
||||
}, [])
|
||||
|
||||
return { accounts, isLoading, error, refetch: fetchAccounts }
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useMemo } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useSession } from '@/lib/auth/auth-client'
|
||||
import type { PermissionType, WorkspacePermissions } from '@/hooks/use-workspace-permissions'
|
||||
import type { WorkspacePermissions } from '@/hooks/queries/workspace'
|
||||
|
||||
export type PermissionType = 'admin' | 'write' | 'read'
|
||||
|
||||
const logger = createLogger('useUserPermissions')
|
||||
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import type { permissionTypeEnum } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { API_ENDPOINTS } from '@/stores/constants'
|
||||
|
||||
const logger = createLogger('useWorkspacePermissions')
|
||||
|
||||
export type PermissionType = (typeof permissionTypeEnum.enumValues)[number]
|
||||
|
||||
export interface WorkspaceUser {
|
||||
userId: string
|
||||
email: string
|
||||
name: string | null
|
||||
image: string | null
|
||||
permissionType: PermissionType
|
||||
}
|
||||
|
||||
export interface WorkspacePermissions {
|
||||
users: WorkspaceUser[]
|
||||
total: number
|
||||
}
|
||||
|
||||
interface UseWorkspacePermissionsReturn {
|
||||
permissions: WorkspacePermissions | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
updatePermissions: (newPermissions: WorkspacePermissions) => void
|
||||
refetch: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to fetch and manage workspace permissions
|
||||
*
|
||||
* @param workspaceId - The workspace ID to fetch permissions for
|
||||
* @returns Object containing permissions data, loading state, error state, and refetch function
|
||||
*/
|
||||
export function useWorkspacePermissions(workspaceId: string | null): UseWorkspacePermissionsReturn {
|
||||
const [permissions, setPermissions] = useState<WorkspacePermissions | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchPermissions = async (id: string): Promise<void> => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const response = await fetch(API_ENDPOINTS.WORKSPACE_PERMISSIONS(id))
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Workspace not found or access denied')
|
||||
}
|
||||
if (response.status === 401) {
|
||||
throw new Error('Authentication required')
|
||||
}
|
||||
throw new Error(`Failed to fetch permissions: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data: WorkspacePermissions = await response.json()
|
||||
setPermissions(data)
|
||||
|
||||
logger.info('Workspace permissions loaded', {
|
||||
workspaceId: id,
|
||||
userCount: data.total,
|
||||
users: data.users.map((u) => ({ email: u.email, permissions: u.permissionType })),
|
||||
})
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'
|
||||
setError(errorMessage)
|
||||
logger.error('Failed to fetch workspace permissions', {
|
||||
workspaceId: id,
|
||||
error: errorMessage,
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const updatePermissions = useCallback((newPermissions: WorkspacePermissions): void => {
|
||||
setPermissions(newPermissions)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceId) {
|
||||
fetchPermissions(workspaceId)
|
||||
} else {
|
||||
// Clear state if no workspace ID
|
||||
setPermissions(null)
|
||||
setError(null)
|
||||
setLoading(false)
|
||||
}
|
||||
}, [workspaceId])
|
||||
|
||||
const refetch = useCallback(async () => {
|
||||
if (workspaceId) {
|
||||
await fetchPermissions(workspaceId)
|
||||
}
|
||||
}, [workspaceId])
|
||||
|
||||
return {
|
||||
permissions,
|
||||
loading,
|
||||
error,
|
||||
updatePermissions,
|
||||
refetch,
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export function sanitizeHeaders(
|
||||
* Client-safe MCP constants
|
||||
*/
|
||||
export const MCP_CLIENT_CONSTANTS = {
|
||||
CLIENT_TIMEOUT: 60000,
|
||||
CLIENT_TIMEOUT: 600000,
|
||||
MAX_RETRIES: 3,
|
||||
RECONNECT_DELAY: 1000,
|
||||
} as const
|
||||
|
||||
@@ -81,8 +81,8 @@ describe('generateMcpServerId', () => {
|
||||
})
|
||||
|
||||
describe('MCP_CONSTANTS', () => {
|
||||
it.concurrent('has correct execution timeout', () => {
|
||||
expect(MCP_CONSTANTS.EXECUTION_TIMEOUT).toBe(60000)
|
||||
it.concurrent('has correct execution timeout (10 minutes)', () => {
|
||||
expect(MCP_CONSTANTS.EXECUTION_TIMEOUT).toBe(600000)
|
||||
})
|
||||
|
||||
it.concurrent('has correct cache timeout (5 minutes)', () => {
|
||||
@@ -107,8 +107,8 @@ describe('MCP_CONSTANTS', () => {
|
||||
})
|
||||
|
||||
describe('MCP_CLIENT_CONSTANTS', () => {
|
||||
it.concurrent('has correct client timeout', () => {
|
||||
expect(MCP_CLIENT_CONSTANTS.CLIENT_TIMEOUT).toBe(60000)
|
||||
it.concurrent('has correct client timeout (10 minutes)', () => {
|
||||
expect(MCP_CLIENT_CONSTANTS.CLIENT_TIMEOUT).toBe(600000)
|
||||
})
|
||||
|
||||
it.concurrent('has correct auto refresh interval (5 minutes)', () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { isMcpTool, MCP } from '@/executor/constants'
|
||||
* MCP-specific constants
|
||||
*/
|
||||
export const MCP_CONSTANTS = {
|
||||
EXECUTION_TIMEOUT: 60000,
|
||||
EXECUTION_TIMEOUT: 600000,
|
||||
CACHE_TIMEOUT: 5 * 60 * 1000,
|
||||
DEFAULT_RETRIES: 3,
|
||||
DEFAULT_CONNECTION_TIMEOUT: 30000,
|
||||
@@ -49,7 +49,7 @@ export function sanitizeHeaders(
|
||||
* Client-safe MCP constants
|
||||
*/
|
||||
export const MCP_CLIENT_CONSTANTS = {
|
||||
CLIENT_TIMEOUT: 60000,
|
||||
CLIENT_TIMEOUT: 600000,
|
||||
AUTO_REFRESH_INTERVAL: 5 * 60 * 1000,
|
||||
} as const
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { A2ACancelTaskParams, A2ACancelTaskResponse } from '@/tools/a2a/types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { A2ACancelTaskParams, A2ACancelTaskResponse } from './types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const a2aCancelTaskTool: ToolConfig<A2ACancelTaskParams, A2ACancelTaskResponse> = {
|
||||
id: 'a2a_cancel_task',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type {
|
||||
A2ADeletePushNotificationParams,
|
||||
A2ADeletePushNotificationResponse,
|
||||
} from '@/tools/a2a/types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { A2ADeletePushNotificationParams, A2ADeletePushNotificationResponse } from './types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const a2aDeletePushNotificationTool: ToolConfig<
|
||||
A2ADeletePushNotificationParams,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { A2AGetAgentCardParams, A2AGetAgentCardResponse } from '@/tools/a2a/types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { A2AGetAgentCardParams, A2AGetAgentCardResponse } from './types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const a2aGetAgentCardTool: ToolConfig<A2AGetAgentCardParams, A2AGetAgentCardResponse> = {
|
||||
id: 'a2a_get_agent_card',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type {
|
||||
A2AGetPushNotificationParams,
|
||||
A2AGetPushNotificationResponse,
|
||||
} from '@/tools/a2a/types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { A2AGetPushNotificationParams, A2AGetPushNotificationResponse } from './types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const a2aGetPushNotificationTool: ToolConfig<
|
||||
A2AGetPushNotificationParams,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { A2AGetTaskParams, A2AGetTaskResponse } from '@/tools/a2a/types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { A2AGetTaskParams, A2AGetTaskResponse } from './types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const a2aGetTaskTool: ToolConfig<A2AGetTaskParams, A2AGetTaskResponse> = {
|
||||
id: 'a2a_get_task',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { A2AResubscribeParams, A2AResubscribeResponse } from '@/tools/a2a/types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { A2AResubscribeParams, A2AResubscribeResponse } from './types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const a2aResubscribeTool: ToolConfig<A2AResubscribeParams, A2AResubscribeResponse> = {
|
||||
id: 'a2a_resubscribe',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { A2ASendMessageParams, A2ASendMessageResponse } from '@/tools/a2a/types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { A2ASendMessageParams, A2ASendMessageResponse } from './types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const a2aSendMessageTool: ToolConfig<A2ASendMessageParams, A2ASendMessageResponse> = {
|
||||
id: 'a2a_send_message',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type {
|
||||
A2ASetPushNotificationParams,
|
||||
A2ASetPushNotificationResponse,
|
||||
} from '@/tools/a2a/types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { A2ASetPushNotificationParams, A2ASetPushNotificationResponse } from './types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const a2aSetPushNotificationTool: ToolConfig<
|
||||
A2ASetPushNotificationParams,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RunActorParams, RunActorResult } from '@/tools/apify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { RunActorParams, RunActorResult } from './types'
|
||||
|
||||
const POLL_INTERVAL_MS = 5000 // 5 seconds between polls
|
||||
const MAX_POLL_TIME_MS = 300000 // 5 minutes maximum polling time
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RunActorParams, RunActorResult } from '@/tools/apify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { RunActorParams, RunActorResult } from './types'
|
||||
|
||||
export const apifyRunActorSyncTool: ToolConfig<RunActorParams, RunActorResult> = {
|
||||
id: 'apify_run_actor_sync',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
GoogleGroupsAddAliasParams,
|
||||
GoogleGroupsAddAliasResponse,
|
||||
} from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsAddAliasParams, GoogleGroupsAddAliasResponse } from './types'
|
||||
|
||||
export const addAliasTool: ToolConfig<GoogleGroupsAddAliasParams, GoogleGroupsAddAliasResponse> = {
|
||||
id: 'google_groups_add_alias',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GoogleGroupsAddMemberParams, GoogleGroupsResponse } from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsAddMemberParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const addMemberTool: ToolConfig<GoogleGroupsAddMemberParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_add_member',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GoogleGroupsCreateParams, GoogleGroupsResponse } from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsCreateParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const createGroupTool: ToolConfig<GoogleGroupsCreateParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_create_group',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GoogleGroupsDeleteParams, GoogleGroupsResponse } from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsDeleteParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const deleteGroupTool: ToolConfig<GoogleGroupsDeleteParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_delete_group',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GoogleGroupsGetParams, GoogleGroupsResponse } from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsGetParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const getGroupTool: ToolConfig<GoogleGroupsGetParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_get_group',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GoogleGroupsGetMemberParams, GoogleGroupsResponse } from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsGetMemberParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const getMemberTool: ToolConfig<GoogleGroupsGetMemberParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_get_member',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
GoogleGroupsGetSettingsParams,
|
||||
GoogleGroupsGetSettingsResponse,
|
||||
} from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsGetSettingsParams, GoogleGroupsGetSettingsResponse } from './types'
|
||||
|
||||
export const getSettingsTool: ToolConfig<
|
||||
GoogleGroupsGetSettingsParams,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GoogleGroupsHasMemberParams, GoogleGroupsResponse } from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsHasMemberParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const hasMemberTool: ToolConfig<GoogleGroupsHasMemberParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_has_member',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
GoogleGroupsListAliasesParams,
|
||||
GoogleGroupsListAliasesResponse,
|
||||
} from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsListAliasesParams, GoogleGroupsListAliasesResponse } from './types'
|
||||
|
||||
export const listAliasesTool: ToolConfig<
|
||||
GoogleGroupsListAliasesParams,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GoogleGroupsListParams, GoogleGroupsResponse } from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsListParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const listGroupsTool: ToolConfig<GoogleGroupsListParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_list_groups',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
GoogleGroupsListMembersParams,
|
||||
GoogleGroupsResponse,
|
||||
} from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsListMembersParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const listMembersTool: ToolConfig<GoogleGroupsListMembersParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_list_members',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
GoogleGroupsRemoveAliasParams,
|
||||
GoogleGroupsRemoveAliasResponse,
|
||||
} from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsRemoveAliasParams, GoogleGroupsRemoveAliasResponse } from './types'
|
||||
|
||||
export const removeAliasTool: ToolConfig<
|
||||
GoogleGroupsRemoveAliasParams,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
GoogleGroupsRemoveMemberParams,
|
||||
GoogleGroupsResponse,
|
||||
} from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsRemoveMemberParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const removeMemberTool: ToolConfig<GoogleGroupsRemoveMemberParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_remove_member',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GoogleGroupsResponse, GoogleGroupsUpdateParams } from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsResponse, GoogleGroupsUpdateParams } from './types'
|
||||
|
||||
export const updateGroupTool: ToolConfig<GoogleGroupsUpdateParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_update_group',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
GoogleGroupsResponse,
|
||||
GoogleGroupsUpdateMemberParams,
|
||||
} from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsResponse, GoogleGroupsUpdateMemberParams } from './types'
|
||||
|
||||
export const updateMemberTool: ToolConfig<GoogleGroupsUpdateMemberParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_update_member',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
GoogleGroupsUpdateSettingsParams,
|
||||
GoogleGroupsUpdateSettingsResponse,
|
||||
} from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsUpdateSettingsParams, GoogleGroupsUpdateSettingsResponse } from './types'
|
||||
|
||||
export const updateSettingsTool: ToolConfig<
|
||||
GoogleGroupsUpdateSettingsParams,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createHmac } from 'crypto'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import type { RequestResponse, WebhookRequestParams } from '@/tools/http/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { RequestResponse, WebhookRequestParams } from './types'
|
||||
|
||||
/**
|
||||
* Generates HMAC-SHA256 signature for webhook payload
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
IncidentioIncidentStatusesListParams,
|
||||
IncidentioIncidentStatusesListResponse,
|
||||
} from './types'
|
||||
} from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const incidentStatusesListTool: ToolConfig<
|
||||
IncidentioIncidentStatusesListParams,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
IncidentioIncidentTypesListParams,
|
||||
IncidentioIncidentTypesListResponse,
|
||||
} from './types'
|
||||
} from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const incidentTypesListTool: ToolConfig<
|
||||
IncidentioIncidentTypesListParams,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
IncidentioSeveritiesListParams,
|
||||
IncidentioSeveritiesListResponse,
|
||||
} from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { IncidentioSeveritiesListParams, IncidentioSeveritiesListResponse } from './types'
|
||||
|
||||
export const severitiesListTool: ToolConfig<
|
||||
IncidentioSeveritiesListParams,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
IncidentioUsersListParams,
|
||||
IncidentioUsersListResponse,
|
||||
} from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { IncidentioUsersListParams, IncidentioUsersListResponse } from './types'
|
||||
|
||||
export const usersListTool: ToolConfig<IncidentioUsersListParams, IncidentioUsersListResponse> = {
|
||||
id: 'incidentio_users_list',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
IncidentioUsersShowParams,
|
||||
IncidentioUsersShowResponse,
|
||||
} from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { IncidentioUsersShowParams, IncidentioUsersShowResponse } from './types'
|
||||
|
||||
export const usersShowTool: ToolConfig<IncidentioUsersShowParams, IncidentioUsersShowResponse> = {
|
||||
id: 'incidentio_users_show',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WorkflowsCreateParams, WorkflowsCreateResponse } from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WorkflowsCreateParams, WorkflowsCreateResponse } from './types'
|
||||
|
||||
export const workflowsCreateTool: ToolConfig<WorkflowsCreateParams, WorkflowsCreateResponse> = {
|
||||
id: 'incidentio_workflows_create',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WorkflowsDeleteParams, WorkflowsDeleteResponse } from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WorkflowsDeleteParams, WorkflowsDeleteResponse } from './types'
|
||||
|
||||
export const workflowsDeleteTool: ToolConfig<WorkflowsDeleteParams, WorkflowsDeleteResponse> = {
|
||||
id: 'incidentio_workflows_delete',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WorkflowsListParams, WorkflowsListResponse } from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WorkflowsListParams, WorkflowsListResponse } from './types'
|
||||
|
||||
export const workflowsListTool: ToolConfig<WorkflowsListParams, WorkflowsListResponse> = {
|
||||
id: 'incidentio_workflows_list',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WorkflowsShowParams, WorkflowsShowResponse } from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WorkflowsShowParams, WorkflowsShowResponse } from './types'
|
||||
|
||||
export const workflowsShowTool: ToolConfig<WorkflowsShowParams, WorkflowsShowResponse> = {
|
||||
id: 'incidentio_workflows_show',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WorkflowsUpdateParams, WorkflowsUpdateResponse } from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WorkflowsUpdateParams, WorkflowsUpdateResponse } from './types'
|
||||
|
||||
export const workflowsUpdateTool: ToolConfig<WorkflowsUpdateParams, WorkflowsUpdateResponse> = {
|
||||
id: 'incidentio_workflows_update',
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('IntercomGetCompany')
|
||||
|
||||
export interface IntercomGetCompanyParams {
|
||||
accessToken: string
|
||||
companyId: string
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('IntercomGetConversation')
|
||||
|
||||
export interface IntercomGetConversationParams {
|
||||
accessToken: string
|
||||
conversationId: string
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('IntercomListCompanies')
|
||||
|
||||
export interface IntercomListCompaniesParams {
|
||||
accessToken: string
|
||||
per_page?: number
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('IntercomListContacts')
|
||||
|
||||
export interface IntercomListContactsParams {
|
||||
accessToken: string
|
||||
per_page?: number
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('IntercomListConversations')
|
||||
|
||||
export interface IntercomListConversationsParams {
|
||||
accessToken: string
|
||||
per_page?: number
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('IntercomReplyConversation')
|
||||
|
||||
export interface IntercomReplyConversationParams {
|
||||
accessToken: string
|
||||
conversationId: string
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('IntercomSearchContacts')
|
||||
|
||||
export interface IntercomSearchContactsParams {
|
||||
accessToken: string
|
||||
query: string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { KalshiAuthParams, KalshiOrder } from '@/tools/kalshi/types'
|
||||
import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { KalshiAuthParams, KalshiOrder } from './types'
|
||||
import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from './types'
|
||||
|
||||
export interface KalshiAmendOrderParams extends KalshiAuthParams {
|
||||
orderId: string // Order ID to amend (required)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { KalshiAuthParams, KalshiOrder } from '@/tools/kalshi/types'
|
||||
import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { KalshiAuthParams, KalshiOrder } from './types'
|
||||
import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from './types'
|
||||
|
||||
export interface KalshiCancelOrderParams extends KalshiAuthParams {
|
||||
orderId: string // Order ID to cancel (required)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { KalshiAuthParams, KalshiOrder } from '@/tools/kalshi/types'
|
||||
import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { KalshiAuthParams, KalshiOrder } from './types'
|
||||
import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from './types'
|
||||
|
||||
export interface KalshiCreateOrderParams extends KalshiAuthParams {
|
||||
ticker: string // Market ticker (required)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { KalshiAuthParams } from '@/tools/kalshi/types'
|
||||
import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { KalshiAuthParams } from './types'
|
||||
import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from './types'
|
||||
|
||||
export interface KalshiGetBalanceParams extends KalshiAuthParams {}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { KalshiCandlestick } from '@/tools/kalshi/types'
|
||||
import { buildKalshiUrl, handleKalshiError } from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { KalshiCandlestick } from './types'
|
||||
import { buildKalshiUrl, handleKalshiError } from './types'
|
||||
|
||||
export interface KalshiGetCandlesticksParams {
|
||||
seriesTicker: string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { KalshiEvent } from '@/tools/kalshi/types'
|
||||
import { buildKalshiUrl, handleKalshiError } from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { KalshiEvent } from './types'
|
||||
import { buildKalshiUrl, handleKalshiError } from './types'
|
||||
|
||||
export interface KalshiGetEventParams {
|
||||
eventTicker: string // Event ticker
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { KalshiEvent, KalshiPaginationParams, KalshiPagingInfo } from '@/tools/kalshi/types'
|
||||
import {
|
||||
buildKalshiUrl,
|
||||
handleKalshiError,
|
||||
KALSHI_EVENT_OUTPUT_PROPERTIES,
|
||||
} from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { KalshiEvent, KalshiPaginationParams, KalshiPagingInfo } from './types'
|
||||
import { buildKalshiUrl, handleKalshiError, KALSHI_EVENT_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export interface KalshiGetEventsParams extends KalshiPaginationParams {
|
||||
status?: string // open, closed, settled
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { KalshiExchangeStatus } from '@/tools/kalshi/types'
|
||||
import { buildKalshiUrl, handleKalshiError } from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { KalshiExchangeStatus } from './types'
|
||||
import { buildKalshiUrl, handleKalshiError } from './types'
|
||||
|
||||
export type KalshiGetExchangeStatusParams = Record<string, never>
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
KalshiAuthParams,
|
||||
KalshiFill,
|
||||
KalshiPaginationParams,
|
||||
KalshiPagingInfo,
|
||||
} from './types'
|
||||
} from '@/tools/kalshi/types'
|
||||
import {
|
||||
buildKalshiAuthHeaders,
|
||||
buildKalshiUrl,
|
||||
handleKalshiError,
|
||||
KALSHI_FILL_OUTPUT_PROPERTIES,
|
||||
} from './types'
|
||||
} from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface KalshiGetFillsParams extends KalshiAuthParams, KalshiPaginationParams {
|
||||
ticker?: string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { KalshiMarket } from '@/tools/kalshi/types'
|
||||
import { buildKalshiUrl, handleKalshiError } from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { KalshiMarket } from './types'
|
||||
import { buildKalshiUrl, handleKalshiError } from './types'
|
||||
|
||||
export interface KalshiGetMarketParams {
|
||||
ticker: string // Market ticker
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { KalshiMarket, KalshiPaginationParams, KalshiPagingInfo } from './types'
|
||||
import type { KalshiMarket, KalshiPaginationParams, KalshiPagingInfo } from '@/tools/kalshi/types'
|
||||
import {
|
||||
buildKalshiUrl,
|
||||
handleKalshiError,
|
||||
KALSHI_MARKET_OUTPUT_PROPERTIES,
|
||||
KALSHI_PAGING_OUTPUT_PROPERTIES,
|
||||
} from './types'
|
||||
} from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface KalshiGetMarketsParams extends KalshiPaginationParams {
|
||||
status?: string // unopened, open, closed, settled
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { KalshiAuthParams, KalshiOrder } from '@/tools/kalshi/types'
|
||||
import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { KalshiAuthParams, KalshiOrder } from './types'
|
||||
import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from './types'
|
||||
|
||||
export interface KalshiGetOrderParams extends KalshiAuthParams {
|
||||
orderId: string // Order ID to retrieve (required)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { KalshiOrderbook } from '@/tools/kalshi/types'
|
||||
import { buildKalshiUrl, handleKalshiError } from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { KalshiOrderbook } from './types'
|
||||
import { buildKalshiUrl, handleKalshiError } from './types'
|
||||
|
||||
export interface KalshiGetOrderbookParams {
|
||||
ticker: string
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
KalshiAuthParams,
|
||||
KalshiOrder,
|
||||
KalshiPaginationParams,
|
||||
KalshiPagingInfo,
|
||||
} from './types'
|
||||
} from '@/tools/kalshi/types'
|
||||
import {
|
||||
buildKalshiAuthHeaders,
|
||||
buildKalshiUrl,
|
||||
handleKalshiError,
|
||||
KALSHI_ORDER_OUTPUT_PROPERTIES,
|
||||
} from './types'
|
||||
} from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface KalshiGetOrdersParams extends KalshiAuthParams, KalshiPaginationParams {
|
||||
ticker?: string
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
KalshiAuthParams,
|
||||
KalshiPaginationParams,
|
||||
KalshiPagingInfo,
|
||||
KalshiPosition,
|
||||
} from './types'
|
||||
} from '@/tools/kalshi/types'
|
||||
import {
|
||||
buildKalshiAuthHeaders,
|
||||
buildKalshiUrl,
|
||||
handleKalshiError,
|
||||
KALSHI_EVENT_POSITION_OUTPUT_PROPERTIES,
|
||||
KALSHI_POSITION_OUTPUT_PROPERTIES,
|
||||
} from './types'
|
||||
} from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface KalshiGetPositionsParams extends KalshiAuthParams, KalshiPaginationParams {
|
||||
ticker?: string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { KalshiSeries } from '@/tools/kalshi/types'
|
||||
import { buildKalshiUrl, handleKalshiError } from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { KalshiSeries } from './types'
|
||||
import { buildKalshiUrl, handleKalshiError } from './types'
|
||||
|
||||
export interface KalshiGetSeriesByTickerParams {
|
||||
seriesTicker: string
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { KalshiPaginationParams, KalshiPagingInfo, KalshiTrade } from '@/tools/kalshi/types'
|
||||
import {
|
||||
buildKalshiUrl,
|
||||
handleKalshiError,
|
||||
KALSHI_TRADE_OUTPUT_PROPERTIES,
|
||||
} from '@/tools/kalshi/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { KalshiPaginationParams, KalshiPagingInfo, KalshiTrade } from './types'
|
||||
import { buildKalshiUrl, handleKalshiError, KALSHI_TRADE_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export interface KalshiGetTradesParams extends KalshiPaginationParams {}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import crypto from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { OutputProperty } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('Kalshi')
|
||||
|
||||
// Base URL for Kalshi API
|
||||
export const KALSHI_BASE_URL = 'https://api.elections.kalshi.com/trade-api/v2'
|
||||
|
||||
@@ -598,8 +595,6 @@ export function buildKalshiAuthHeaders(
|
||||
|
||||
// Helper function for consistent error handling
|
||||
export function handleKalshiError(data: any, status: number, operation: string): never {
|
||||
logger.error(`Kalshi API request failed for ${operation}`, { data, status })
|
||||
|
||||
const errorMessage =
|
||||
data.error?.message || data.error || data.message || data.detail || 'Unknown error'
|
||||
throw new Error(`Kalshi ${operation} failed: ${errorMessage}`)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import {
|
||||
buildMailchimpUrl,
|
||||
handleMailchimpError,
|
||||
type MailchimpMember,
|
||||
} from '@/tools/mailchimp/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import { buildMailchimpUrl, handleMailchimpError, type MailchimpMember } from './types'
|
||||
|
||||
const logger = createLogger('MailchimpAddMember')
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from '@/tools/mailchimp/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from './types'
|
||||
|
||||
const logger = createLogger('MailchimpAddMemberTags')
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import {
|
||||
buildMailchimpUrl,
|
||||
handleMailchimpError,
|
||||
type MailchimpMember,
|
||||
} from '@/tools/mailchimp/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import { buildMailchimpUrl, handleMailchimpError, type MailchimpMember } from './types'
|
||||
|
||||
const logger = createLogger('MailchimpAddOrUpdateMember')
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import {
|
||||
buildMailchimpUrl,
|
||||
handleMailchimpError,
|
||||
type MailchimpMember,
|
||||
} from '@/tools/mailchimp/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import { buildMailchimpUrl, handleMailchimpError, type MailchimpMember } from './types'
|
||||
|
||||
const logger = createLogger('MailchimpAddSegmentMember')
|
||||
|
||||
export interface MailchimpAddSegmentMemberParams {
|
||||
apiKey: string
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { MailchimpMember } from '@/tools/mailchimp/types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from '@/tools/mailchimp/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { MailchimpMember } from './types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from './types'
|
||||
|
||||
const logger = createLogger('MailchimpAddSubscriberToAutomation')
|
||||
|
||||
export interface MailchimpAddSubscriberToAutomationParams {
|
||||
apiKey: string
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from '@/tools/mailchimp/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from './types'
|
||||
|
||||
const logger = createLogger('MailchimpArchiveMember')
|
||||
|
||||
export interface MailchimpArchiveMemberParams {
|
||||
apiKey: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { MailchimpAudience } from '@/tools/mailchimp/types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from '@/tools/mailchimp/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { MailchimpAudience } from './types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from './types'
|
||||
|
||||
const logger = createLogger('MailchimpCreateAudience')
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { MailchimpBatchOperation } from '@/tools/mailchimp/types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from '@/tools/mailchimp/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { MailchimpBatchOperation } from './types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from './types'
|
||||
|
||||
const logger = createLogger('MailchimpCreateBatchOperation')
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import {
|
||||
buildMailchimpUrl,
|
||||
handleMailchimpError,
|
||||
type MailchimpCampaign,
|
||||
} from '@/tools/mailchimp/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import { buildMailchimpUrl, handleMailchimpError, type MailchimpCampaign } from './types'
|
||||
|
||||
const logger = createLogger('MailchimpCreateCampaign')
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { MailchimpInterest } from '@/tools/mailchimp/types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from '@/tools/mailchimp/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { MailchimpInterest } from './types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from './types'
|
||||
|
||||
const logger = createLogger('MailchimpCreateInterest')
|
||||
|
||||
export interface MailchimpCreateInterestParams {
|
||||
apiKey: string
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { MailchimpInterestCategory } from '@/tools/mailchimp/types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from '@/tools/mailchimp/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { MailchimpInterestCategory } from './types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from './types'
|
||||
|
||||
const logger = createLogger('MailchimpCreateInterestCategory')
|
||||
|
||||
export interface MailchimpCreateInterestCategoryParams {
|
||||
apiKey: string
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { MailchimpLandingPage } from '@/tools/mailchimp/types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from '@/tools/mailchimp/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { MailchimpLandingPage } from './types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from './types'
|
||||
|
||||
const logger = createLogger('MailchimpCreateLandingPage')
|
||||
|
||||
export interface MailchimpCreateLandingPageParams {
|
||||
apiKey: string
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { MailchimpMergeField } from '@/tools/mailchimp/types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from '@/tools/mailchimp/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { MailchimpMergeField } from './types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from './types'
|
||||
|
||||
const logger = createLogger('MailchimpCreateMergeField')
|
||||
|
||||
export interface MailchimpCreateMergeFieldParams {
|
||||
apiKey: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { MailchimpSegment } from '@/tools/mailchimp/types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from '@/tools/mailchimp/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { MailchimpSegment } from './types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from './types'
|
||||
|
||||
const logger = createLogger('MailchimpCreateSegment')
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from '@/tools/mailchimp/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from './types'
|
||||
|
||||
const logger = createLogger('MailchimpCreateTemplate')
|
||||
|
||||
export interface MailchimpCreateTemplateParams {
|
||||
apiKey: string
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from '@/tools/mailchimp/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import { buildMailchimpUrl, handleMailchimpError } from './types'
|
||||
|
||||
const logger = createLogger('MailchimpDeleteAudience')
|
||||
|
||||
export interface MailchimpDeleteAudienceParams {
|
||||
apiKey: string
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user