diff --git a/apps/sim/app/api/users/me/subscription/[id]/transfer/route.test.ts b/apps/sim/app/api/users/me/subscription/[id]/transfer/route.test.ts
deleted file mode 100644
index 77192d8d77..0000000000
--- a/apps/sim/app/api/users/me/subscription/[id]/transfer/route.test.ts
+++ /dev/null
@@ -1,278 +0,0 @@
-/**
- * Tests for Subscription Transfer API
- *
- * @vitest-environment node
- */
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import {
- createMockRequest,
- mockAdminMember,
- mockDb,
- mockLogger,
- mockOrganization,
- mockRegularMember,
- mockSubscription,
- mockUser,
-} from '@/app/api/__test-utils__/utils'
-
-describe('Subscription Transfer API Routes', () => {
- beforeEach(() => {
- vi.resetModules()
-
- vi.doMock('@/lib/auth', () => ({
- getSession: vi.fn().mockResolvedValue({
- user: mockUser,
- }),
- }))
-
- vi.doMock('@/lib/logs/console/logger', () => ({
- createLogger: vi.fn().mockReturnValue(mockLogger),
- }))
-
- vi.doMock('@sim/db', () => ({
- db: mockDb,
- }))
-
- mockDb.select.mockReturnValue({
- from: vi.fn().mockReturnThis(),
- where: vi.fn().mockReturnThis(),
- then: vi.fn().mockResolvedValue([mockSubscription]),
- })
-
- mockDb.update.mockReturnValue({
- set: vi.fn().mockReturnThis(),
- where: vi.fn().mockResolvedValue([{ affected: 1 }]),
- })
- })
-
- afterEach(() => {
- vi.clearAllMocks()
- })
-
- describe('POST handler', () => {
- it('should successfully transfer a personal subscription to an organization', async () => {
- vi.doMock('@/lib/auth', () => ({
- getSession: vi.fn().mockResolvedValue({
- user: {
- ...mockUser,
- id: 'user-123',
- },
- }),
- }))
-
- vi.doMock('@sim/db/schema', () => ({
- subscription: { id: 'id', referenceId: 'referenceId' },
- organization: { id: 'id' },
- member: { userId: 'userId', organizationId: 'organizationId', role: 'role' },
- }))
-
- const mockSubscriptionWithReferenceId = {
- ...mockSubscription,
- referenceId: 'user-123',
- }
-
- mockDb.select.mockImplementation(() => {
- return {
- from: () => ({
- where: () => {
- if (mockDb.select.mock.calls.length === 1) {
- return Promise.resolve([mockSubscriptionWithReferenceId])
- }
- if (mockDb.select.mock.calls.length === 2) {
- return Promise.resolve([mockOrganization])
- }
- return Promise.resolve([mockAdminMember])
- },
- }),
- }
- })
-
- mockDb.update.mockReturnValue({
- set: () => ({
- where: () => Promise.resolve({ affected: 1 }),
- }),
- })
-
- const req = createMockRequest('POST', {
- organizationId: 'org-456',
- })
-
- const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route')
-
- const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) })
-
- const data = await response.json()
-
- expect(response.status).toBe(200)
- expect(data).toHaveProperty('success', true)
- expect(data).toHaveProperty('message', 'Subscription transferred successfully')
- expect(mockDb.update).toHaveBeenCalled()
- })
-
- it('should test behavior when subscription not found', async () => {
- mockDb.select.mockReturnValueOnce({
- from: vi.fn().mockReturnThis(),
- where: vi.fn().mockReturnThis(),
- then: vi.fn().mockResolvedValue([]),
- })
-
- const req = createMockRequest('POST', {
- organizationId: 'org-456',
- })
-
- const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route')
-
- const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) })
- const data = await response.json()
-
- expect(response.status).toBe(403)
- expect(data).toHaveProperty('error', 'Unauthorized - subscription does not belong to user')
- })
-
- it('should test behavior when organization not found', async () => {
- const mockSelectImpl = vi
- .fn()
- .mockReturnValueOnce({
- from: vi.fn().mockReturnThis(),
- where: vi.fn().mockReturnThis(),
- then: vi.fn().mockResolvedValue([mockSubscription]),
- })
- .mockReturnValueOnce({
- from: vi.fn().mockReturnThis(),
- where: vi.fn().mockReturnThis(),
- then: vi.fn().mockResolvedValue([]),
- })
-
- mockDb.select.mockImplementation(mockSelectImpl)
-
- const req = createMockRequest('POST', {
- organizationId: 'org-456',
- })
-
- const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route')
-
- const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) })
- const data = await response.json()
-
- expect(response.status).toBe(403)
- expect(data).toHaveProperty('error', 'Unauthorized - subscription does not belong to user')
- })
-
- it('should reject transfer if user is not the subscription owner', async () => {
- const differentOwnerSubscription = {
- ...mockSubscription,
- referenceId: 'different-user-123',
- }
-
- mockDb.select.mockReturnValueOnce({
- from: vi.fn().mockReturnThis(),
- where: vi.fn().mockReturnThis(),
- then: vi.fn().mockResolvedValue([differentOwnerSubscription]),
- })
-
- const req = createMockRequest('POST', {
- organizationId: 'org-456',
- })
-
- const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route')
-
- const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) })
- const data = await response.json()
-
- expect(response.status).toBe(403)
- expect(data).toHaveProperty('error', 'Unauthorized - subscription does not belong to user')
- expect(mockDb.update).not.toHaveBeenCalled()
- })
-
- it('should reject non-personal transfer if user is not admin of organization', async () => {
- const orgOwnedSubscription = {
- ...mockSubscription,
- referenceId: 'other-org-789',
- }
-
- const mockSelectImpl = vi
- .fn()
- .mockReturnValueOnce({
- from: vi.fn().mockReturnThis(),
- where: vi.fn().mockReturnThis(),
- then: vi.fn().mockResolvedValue([orgOwnedSubscription]),
- })
- .mockReturnValueOnce({
- from: vi.fn().mockReturnThis(),
- where: vi.fn().mockReturnThis(),
- then: vi.fn().mockResolvedValue([mockOrganization]),
- })
- .mockReturnValueOnce({
- from: vi.fn().mockReturnThis(),
- where: vi.fn().mockReturnThis(),
- then: vi.fn().mockResolvedValue([mockRegularMember]),
- })
-
- mockDb.select.mockImplementation(mockSelectImpl)
-
- const req = createMockRequest('POST', {
- organizationId: 'org-456',
- })
-
- const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route')
-
- const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) })
- const data = await response.json()
-
- expect(response.status).toBe(403)
- expect(data).toHaveProperty('error', 'Unauthorized - subscription does not belong to user')
- expect(mockDb.update).not.toHaveBeenCalled()
- })
-
- it('should reject invalid request parameters', async () => {
- const req = createMockRequest('POST', {})
-
- const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route')
-
- const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) })
- const data = await response.json()
-
- expect(response.status).toBe(400)
- expect(data).toHaveProperty('error', 'Invalid request parameters')
- expect(mockDb.update).not.toHaveBeenCalled()
- })
-
- it('should handle authentication error', async () => {
- vi.doMock('@/lib/auth', () => ({
- getSession: vi.fn().mockResolvedValue(null),
- }))
-
- const req = createMockRequest('POST', {
- organizationId: 'org-456',
- })
-
- const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route')
-
- const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) })
- const data = await response.json()
-
- expect(response.status).toBe(401)
- expect(data).toHaveProperty('error', 'Unauthorized')
- expect(mockDb.update).not.toHaveBeenCalled()
- })
-
- it('should handle internal server error', async () => {
- mockDb.select.mockImplementation(() => {
- throw new Error('Database error')
- })
-
- const req = createMockRequest('POST', {
- organizationId: 'org-456',
- })
-
- const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route')
-
- const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) })
- const data = await response.json()
-
- expect(response.status).toBe(500)
- expect(data).toHaveProperty('error', 'Failed to transfer subscription')
- expect(mockLogger.error).toHaveBeenCalled()
- })
- })
-})
diff --git a/apps/sim/app/api/users/me/subscription/[id]/transfer/route.ts b/apps/sim/app/api/users/me/subscription/[id]/transfer/route.ts
index 3a2986b537..a20f600b7d 100644
--- a/apps/sim/app/api/users/me/subscription/[id]/transfer/route.ts
+++ b/apps/sim/app/api/users/me/subscription/[id]/transfer/route.ts
@@ -81,9 +81,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
.where(and(eq(member.userId, session.user.id), eq(member.organizationId, organizationId)))
.then((rows) => rows[0])
- const isPersonalTransfer = sub.referenceId === session.user.id
-
- if (!isPersonalTransfer && (!mem || (mem.role !== 'owner' && mem.role !== 'admin'))) {
+ if (!mem || (mem.role !== 'owner' && mem.role !== 'admin')) {
return NextResponse.json(
{ error: 'Unauthorized - user is not admin of organization' },
{ status: 403 }
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans.tsx
index 49c670b251..e3ab22cd78 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans.tsx
@@ -135,18 +135,22 @@ export function TraceSpans({ traceSpans, totalDuration = 0, onExpansionChange }:
.filter(([, v]) => v)
.map(([k]) => k)
)
- const filterTree = (spans: TraceSpan[]): TraceSpan[] =>
+ const filterTree = (spans: TraceSpan[], parentIsWorkflow = false): TraceSpan[] =>
spans
- .map((s) => ({ ...s }))
+ .map((s) => normalizeChildWorkflowSpan(s))
.filter((s) => {
const tl = s.type?.toLowerCase?.() || ''
if (tl === 'workflow') return true
+ if (parentIsWorkflow) return true
return allowed.has(tl)
})
- .map((s) => ({
- ...s,
- children: s.children ? filterTree(s.children) : undefined,
- }))
+ .map((s) => {
+ const tl = s.type?.toLowerCase?.() || ''
+ return {
+ ...s,
+ children: s.children ? filterTree(s.children, tl === 'workflow') : undefined,
+ }
+ })
return traceSpans ? filterTree(traceSpans) : []
}, [traceSpans, effectiveTypeFilters])
@@ -181,7 +185,6 @@ export function TraceSpans({ traceSpans, totalDuration = 0, onExpansionChange }:
return () => ro.disconnect()
}, [])
- // Early return after all hooks are declared to comply with React's Rules of Hooks
if (!traceSpans || traceSpans.length === 0) {
return
No trace data available
}
@@ -217,21 +220,19 @@ export function TraceSpans({ traceSpans, totalDuration = 0, onExpansionChange }:
{filtered.map((span, index) => {
- const normalizedSpan = normalizeChildWorkflowSpan(span)
const hasSubItems = Boolean(
- (normalizedSpan.children && normalizedSpan.children.length > 0) ||
- (normalizedSpan.toolCalls && normalizedSpan.toolCalls.length > 0) ||
- normalizedSpan.input ||
- normalizedSpan.output
+ (span.children && span.children.length > 0) ||
+ (span.toolCalls && span.toolCalls.length > 0) ||
+ span.input ||
+ span.output
)
- // Calculate gap from previous span (for sequential execution visualization)
let gapMs = 0
let gapPercent = 0
if (index > 0) {
const prevSpan = filtered[index - 1]
const prevEndTime = new Date(prevSpan.endTime).getTime()
- const currentStartTime = new Date(normalizedSpan.startTime).getTime()
+ const currentStartTime = new Date(span.startTime).getTime()
gapMs = currentStartTime - prevEndTime
if (gapMs > 0 && actualTotalDuration > 0) {
gapPercent = (gapMs / actualTotalDuration) * 100
@@ -241,13 +242,13 @@ export function TraceSpans({ traceSpans, totalDuration = 0, onExpansionChange }:
return (
(null)
- const [usageExceeded, setUsageExceeded] = useState(false)
- const [isLoading, setIsLoading] = useState(false)
- const [isUpdating, setIsUpdating] = useState(false)
- const [error, setError] = useState(null)
-
- /**
- * Check user/organization usage limits with caching
- */
- const checkUsage = useCallback(
- async (forceRefresh = false): Promise => {
- const now = Date.now()
- const cacheAge = now - usageDataCache.timestamp
-
- // Return cached data if still valid and not forcing refresh
- if (!forceRefresh && usageDataCache.data && cacheAge < usageDataCache.expirationMs) {
- logger.info('Using cached usage data', {
- cacheAge: `${Math.round(cacheAge / 1000)}s`,
- })
- return usageDataCache.data
- }
-
- setIsLoading(true)
- setError(null)
-
- try {
- // Build query params
- const params = new URLSearchParams({ context })
- if (context === 'organization' && organizationId) {
- params.append('organizationId', organizationId)
- }
-
- // Primary: call server-side usage check to mirror backend enforcement
- const res = await fetch(`/api/usage?${params.toString()}`, { cache: 'no-store' })
- if (res.ok) {
- const payload = await res.json()
- const usage = normalizeUsageData(payload?.data)
-
- // Update cache
- usageDataCache = {
- data: usage,
- timestamp: now,
- expirationMs: usageDataCache.expirationMs,
- }
-
- setUsageData(usage)
- setUsageExceeded(usage?.isExceeded || false)
- return usage
- }
-
- // No fallback available - React Query handles this globally
- throw new Error('Failed to fetch usage data')
- } catch (err) {
- const error = err instanceof Error ? err : new Error('Failed to check usage limits')
- logger.error('Error checking usage limits:', { error })
- setError(error)
- return null
- } finally {
- setIsLoading(false)
- }
- },
- [context, organizationId]
- )
-
- /**
- * Update usage limit for user or organization
- */
- const updateLimit = useCallback(
- async (newLimit: number): Promise<{ success: boolean; error?: string }> => {
- setIsUpdating(true)
- setError(null)
-
- try {
- if (context === 'organization') {
- if (!organizationId) {
- throw new Error('Organization ID is required')
- }
-
- const response = await fetch('/api/usage', {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ context: 'organization', organizationId, limit: newLimit }),
- })
-
- const data = await response.json()
- if (!response.ok) {
- throw new Error(data.error || 'Failed to update limit')
- }
-
- // Clear cache and refresh
- clearCache()
- await checkUsage(true)
-
- return { success: true }
- }
-
- // User context - use API directly
- const response = await fetch('/api/usage', {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ context: 'user', limit: newLimit }),
- })
-
- const data = await response.json()
- if (!response.ok) {
- throw new Error(data.error || 'Failed to update limit')
- }
-
- // Clear cache and refresh
- clearCache()
- await checkUsage(true)
-
- return { success: true }
- } catch (err) {
- const errorMessage = err instanceof Error ? err.message : 'Failed to update usage limit'
- logger.error('Failed to update usage limit', { error: err })
- setError(err instanceof Error ? err : new Error(errorMessage))
- return { success: false, error: errorMessage }
- } finally {
- setIsUpdating(false)
- }
- },
- [context, organizationId, checkUsage]
- )
-
- /**
- * Refresh usage data, bypassing cache
- */
- const refresh = useCallback(async () => {
- return checkUsage(true)
- }, [checkUsage])
-
- /**
- * Clear the cache (useful for testing or forced refresh)
- */
- const clearCache = useCallback(() => {
- usageDataCache = {
- data: null,
- timestamp: 0,
- expirationMs: usageDataCache.expirationMs,
- }
- }, [])
+ // For now, we only support user context via React Query
+ // Organization context should use useOrganizationBilling directly
+ const { data: subscriptionData, isLoading } = useSubscriptionData()
- /**
- * Auto-refresh on mount if enabled
- */
- useEffect(() => {
- if (autoRefresh) {
- checkUsage()
- }
- }, [autoRefresh, checkUsage])
+ const usageExceeded = subscriptionData?.data?.usage?.isExceeded || false
return {
- usageData,
usageExceeded,
isLoading,
- isUpdating,
- error,
- checkUsage,
- refresh,
- updateLimit,
- clearCache,
}
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts
index b0f82b326c..1ff1dd08a6 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts
@@ -1,4 +1,5 @@
import { useCallback, useState } from 'react'
+import { useQueryClient } from '@tanstack/react-query'
import { v4 as uuidv4 } from 'uuid'
import { shallow } from 'zustand/shallow'
import { createLogger } from '@/lib/logs/console/logger'
@@ -11,6 +12,7 @@ import {
} from '@/lib/workflows/trigger-utils'
import { resolveStartCandidates, StartBlockPath, TriggerUtils } from '@/lib/workflows/triggers'
import type { BlockLog, ExecutionResult, StreamingExecution } from '@/executor/types'
+import { subscriptionKeys } from '@/hooks/queries/subscription'
import { useExecutionStream } from '@/hooks/use-execution-stream'
import { WorkflowValidationError } from '@/serializer'
import { useExecutionStore } from '@/stores/execution/store'
@@ -84,6 +86,7 @@ function extractExecutionResult(error: unknown): ExecutionResult | null {
export function useWorkflowExecution() {
const currentWorkflow = useCurrentWorkflow()
const { activeWorkflowId, workflows } = useWorkflowRegistry()
+ const queryClient = useQueryClient()
const { toggleConsole, addConsole } = useTerminalConsoleStore()
const { getAllVariables } = useEnvironmentStore()
const { getVariablesByWorkflowId, variables } = useVariablesStore()
@@ -416,7 +419,7 @@ export function useWorkflowExecution() {
if (!streamingExecution.stream) return
const reader = streamingExecution.stream.getReader()
const blockId = (streamingExecution.execution as any)?.blockId
- const streamStartTime = Date.now()
+
let isFirstChunk = true
if (blockId) {
@@ -577,6 +580,10 @@ export function useWorkflowExecution() {
logger.info(`Processed ${processedCount} blocks for streaming tokenization`)
}
+ // Invalidate subscription query to update usage
+ queryClient.invalidateQueries({ queryKey: subscriptionKeys.user() })
+ queryClient.invalidateQueries({ queryKey: subscriptionKeys.usage() })
+
const { encodeSSE } = await import('@/lib/utils')
controller.enqueue(encodeSSE({ event: 'final', data: result }))
// Note: Logs are already persisted server-side via execution-core.ts
@@ -639,6 +646,10 @@ export function useWorkflowExecution() {
}
;(result.metadata as any).source = 'chat'
}
+
+ // Invalidate subscription query to update usage
+ queryClient.invalidateQueries({ queryKey: subscriptionKeys.user() })
+ queryClient.invalidateQueries({ queryKey: subscriptionKeys.usage() })
}
return result
} catch (error: any) {
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/settings-navigation/settings-navigation.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/settings-navigation/settings-navigation.tsx
index 3f1c159866..2f198c8b2d 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/settings-navigation/settings-navigation.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/settings-navigation/settings-navigation.tsx
@@ -194,7 +194,13 @@ export function SettingsNavigation({
return false
}
- if (item.requiresTeam && !hasOrganization) {
+ if (item.requiresTeam) {
+ const isMember = userRole === 'member' || isAdmin
+ const hasTeamPlan = subscriptionStatus.isTeam || subscriptionStatus.isEnterprise
+
+ if (isMember) return true
+ if (isOwner && hasTeamPlan) return true
+
return false
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/components/usage-limit/usage-limit.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/components/usage-limit/usage-limit.tsx
index d33f01f2c4..8539c8a5a1 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/components/usage-limit/usage-limit.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/components/usage-limit/usage-limit.tsx
@@ -5,7 +5,7 @@ import { Check, Pencil, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
-import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/hooks'
+import { useUpdateOrganizationUsageLimit } from '@/hooks/queries/organization'
import { useUpdateUsageLimit } from '@/hooks/queries/subscription'
const logger = createLogger('UsageLimit')
@@ -41,22 +41,22 @@ export const UsageLimit = forwardRef(
const [hasError, setHasError] = useState(false)
const [errorType, setErrorType] = useState<'general' | 'belowUsage' | null>(null)
const [isEditing, setIsEditing] = useState(false)
+ const [pendingLimit, setPendingLimit] = useState(null)
const inputRef = useRef(null)
- const { updateLimit, isUpdating: isOrgUpdating } = useUsageLimits({
- context,
- organizationId,
- autoRefresh: false, // Don't auto-refresh, we receive values via props
- })
+ const updateUserLimitMutation = useUpdateUsageLimit()
+ const updateOrgLimitMutation = useUpdateOrganizationUsageLimit()
- const updateUsageLimitMutation = useUpdateUsageLimit()
const isUpdating =
- context === 'organization' ? isOrgUpdating : updateUsageLimitMutation.isPending
+ context === 'organization'
+ ? updateOrgLimitMutation.isPending
+ : updateUserLimitMutation.isPending
const handleStartEdit = () => {
if (!canEdit) return
setIsEditing(true)
- setInputValue(currentLimit.toString())
+ const displayLimit = pendingLimit !== null ? pendingLimit : currentLimit
+ setInputValue(displayLimit.toString())
}
useImperativeHandle(
@@ -64,12 +64,19 @@ export const UsageLimit = forwardRef(
() => ({
startEdit: handleStartEdit,
}),
- [canEdit, currentLimit]
+ [canEdit, currentLimit, pendingLimit]
)
useEffect(() => {
- setInputValue(currentLimit.toString())
- }, [currentLimit])
+ if (pendingLimit !== null) {
+ if (currentLimit === pendingLimit) {
+ setPendingLimit(null)
+ setInputValue(currentLimit.toString())
+ }
+ } else {
+ setInputValue(currentLimit.toString())
+ }
+ }, [currentLimit, pendingLimit])
useEffect(() => {
if (isEditing && inputRef.current) {
@@ -110,31 +117,19 @@ export const UsageLimit = forwardRef(
try {
if (context === 'organization') {
- const result = await updateLimit(newLimit)
-
- if (result.success) {
- setInputValue(newLimit.toString())
- onLimitUpdated?.(newLimit)
- setIsEditing(false)
- setErrorType(null)
- setHasError(false)
- } else {
- logger.error('Failed to update usage limit', { error: result.error })
-
- if (result.error?.includes('below current usage')) {
- setErrorType('belowUsage')
- } else {
- setErrorType('general')
- }
-
+ if (!organizationId) {
+ logger.error('Organization ID is required for organization context')
+ setErrorType('general')
setHasError(true)
+ return
}
- return
+ await updateOrgLimitMutation.mutateAsync({ organizationId, limit: newLimit })
+ } else {
+ await updateUserLimitMutation.mutateAsync({ limit: newLimit })
}
- await updateUsageLimitMutation.mutateAsync({ limit: newLimit })
-
+ setPendingLimit(newLimit)
setInputValue(newLimit.toString())
onLimitUpdated?.(newLimit)
setIsEditing(false)
@@ -150,13 +145,16 @@ export const UsageLimit = forwardRef(
setErrorType('general')
}
+ setPendingLimit(null)
+ setInputValue(currentLimit.toString())
setHasError(true)
}
}
const handleCancelEdit = () => {
setIsEditing(false)
- setInputValue(currentLimit.toString())
+ const displayLimit = pendingLimit !== null ? pendingLimit : currentLimit
+ setInputValue(displayLimit.toString())
setHasError(false)
setErrorType(null)
}
@@ -206,7 +204,9 @@ export const UsageLimit = forwardRef(
/>
>
) : (
- ${currentLimit}
+
+ ${pendingLimit !== null ? pendingLimit : currentLimit}
+
)}
{canEdit && (