Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/sim/app/_shell/providers/get-query-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function makeQueryClient() {
retryOnMount: false,
},
mutations: {
retry: 1,
retry: false,
},
dehydrate: {
shouldDehydrateQuery: (query) =>
Expand Down
6 changes: 5 additions & 1 deletion apps/sim/app/api/knowledge/[id]/restore/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { restoreKnowledgeBase } from '@/lib/knowledge/service'
import { KnowledgeBaseConflictError, restoreKnowledgeBase } from '@/lib/knowledge/service'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('RestoreKnowledgeBaseAPI')
Expand Down Expand Up @@ -49,6 +49,10 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{

return NextResponse.json({ success: true })
} catch (error) {
if (error instanceof KnowledgeBaseConflictError) {
return NextResponse.json({ error: error.message }, { status: 409 })
}

logger.error(`[${requestId}] Error restoring knowledge base ${id}`, error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Internal server error' },
Expand Down
14 changes: 9 additions & 5 deletions apps/sim/app/api/knowledge/[id]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,15 @@ vi.mock('@sim/db/schema', () => ({

vi.mock('@/lib/audit/log', () => auditMock)

vi.mock('@/lib/knowledge/service', () => ({
getKnowledgeBaseById: vi.fn(),
updateKnowledgeBase: vi.fn(),
deleteKnowledgeBase: vi.fn(),
}))
vi.mock('@/lib/knowledge/service', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/knowledge/service')>()
return {
...actual,
getKnowledgeBaseById: vi.fn(),
updateKnowledgeBase: vi.fn(),
deleteKnowledgeBase: vi.fn(),
}
})

vi.mock('@/app/api/knowledge/utils', () => ({
checkKnowledgeBaseAccess: vi.fn(),
Expand Down
5 changes: 5 additions & 0 deletions apps/sim/app/api/knowledge/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import {
deleteKnowledgeBase,
getKnowledgeBaseById,
KnowledgeBaseConflictError,
updateKnowledgeBase,
} from '@/lib/knowledge/service'
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
Expand Down Expand Up @@ -166,6 +167,10 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
throw validationError
}
} catch (error) {
if (error instanceof KnowledgeBaseConflictError) {
return NextResponse.json({ error: error.message }, { status: 409 })
}

logger.error(`[${requestId}] Error updating knowledge base`, error)
return NextResponse.json({ error: 'Failed to update knowledge base' }, { status: 500 })
}
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/knowledge/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const { mockGetSession, mockDbChain } = vi.hoisted(() => {
where: vi.fn().mockReturnThis(),
groupBy: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockResolvedValue([]),
limit: vi.fn().mockResolvedValue([]),
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockResolvedValue(undefined),
}
Expand Down Expand Up @@ -113,7 +114,7 @@ describe('Knowledge Base API Route', () => {
Object.values(mockDbChain).forEach((fn) => {
if (typeof fn === 'function') {
fn.mockClear()
if (fn !== mockDbChain.orderBy && fn !== mockDbChain.values) {
if (fn !== mockDbChain.orderBy && fn !== mockDbChain.values && fn !== mockDbChain.limit) {
fn.mockReturnThis()
}
}
Expand Down
5 changes: 5 additions & 0 deletions apps/sim/app/api/knowledge/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import {
createKnowledgeBase,
getKnowledgeBases,
KnowledgeBaseConflictError,
type KnowledgeBaseScope,
} from '@/lib/knowledge/service'

Expand Down Expand Up @@ -149,6 +150,10 @@ export async function POST(req: NextRequest) {
throw validationError
}
} catch (error) {
if (error instanceof KnowledgeBaseConflictError) {
return NextResponse.json({ error: error.message }, { status: 409 })
}

logger.error(`[${requestId}] Error creating knowledge base`, error)
return NextResponse.json({ error: 'Failed to create knowledge base' }, { status: 500 })
}
Expand Down
6 changes: 5 additions & 1 deletion apps/sim/app/api/table/[tableId]/restore/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { getTableById, restoreTable } from '@/lib/table'
import { getTableById, restoreTable, TableConflictError } from '@/lib/table'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('RestoreTableAPI')
Expand Down Expand Up @@ -36,6 +36,10 @@ export async function POST(

return NextResponse.json({ success: true })
} catch (error) {
if (error instanceof TableConflictError) {
return NextResponse.json({ error: error.message }, { status: 409 })
}

logger.error(`[${requestId}] Error restoring table ${tableId}`, error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Internal server error' },
Expand Down
13 changes: 12 additions & 1 deletion apps/sim/app/api/table/[tableId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { deleteTable, NAME_PATTERN, renameTable, TABLE_LIMITS, type TableSchema } from '@/lib/table'
import {
deleteTable,
NAME_PATTERN,
renameTable,
TABLE_LIMITS,
TableConflictError,
type TableSchema,
} from '@/lib/table'
import { accessError, checkAccess, normalizeColumn } from '@/app/api/table/utils'

const logger = createLogger('TableDetailAPI')
Expand Down Expand Up @@ -136,6 +143,10 @@ export async function PATCH(request: NextRequest, { params }: TableRouteParams)
)
}

if (error instanceof TableConflictError) {
return NextResponse.json({ error: error.message }, { status: 409 })
}

logger.error(`[${requestId}] Error renaming table:`, error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to rename table' },
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/app/api/v1/files/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { generateRequestId } from '@/lib/core/utils/request'
import {
FileConflictError,
getWorkspaceFile,
listWorkspaceFiles,
uploadWorkspaceFile,
Expand Down Expand Up @@ -182,7 +183,8 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to upload file'
const isDuplicate = errorMessage.includes('already exists')
const isDuplicate =
error instanceof FileConflictError || errorMessage.includes('already exists')

if (isDuplicate) {
return NextResponse.json({ error: errorMessage }, { status: 409 })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { restoreWorkspaceFile } from '@/lib/uploads/contexts/workspace'
import { FileConflictError, restoreWorkspaceFile } from '@/lib/uploads/contexts/workspace'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('RestoreWorkspaceFileAPI')
Expand Down Expand Up @@ -31,6 +31,9 @@ export async function POST(

return NextResponse.json({ success: true })
} catch (error) {
if (error instanceof FileConflictError) {
return NextResponse.json({ error: error.message }, { status: 409 })
}
logger.error(`[${requestId}] Error restoring workspace file ${fileId}`, error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Internal server error' },
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/app/api/workspaces/[id]/files/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import {
FileConflictError,
listWorkspaceFiles,
uploadWorkspaceFile,
type WorkspaceFileScope,
Expand Down Expand Up @@ -135,9 +136,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
} catch (error) {
logger.error(`[${requestId}] Error uploading workspace file:`, error)

// Check if it's a duplicate file error
const errorMessage = error instanceof Error ? error.message : 'Failed to upload file'
const isDuplicate = errorMessage.includes('already exists')
const isDuplicate =
error instanceof FileConflictError || errorMessage.includes('already exists')

return NextResponse.json(
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { X } from 'lucide-react'
import { Button, Tooltip } from '@/components/emcn'
import { Button, CountdownRing, Tooltip } from '@/components/emcn'
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
Expand All @@ -20,9 +20,6 @@ const STACK_OFFSET_PX = 3
const AUTO_DISMISS_MS = 10000
const EXIT_ANIMATION_MS = 200

const RING_RADIUS = 5.5
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS

const ACTION_LABELS: Record<NotificationAction['type'], string> = {
copilot: 'Fix in Copilot',
refresh: 'Refresh',
Expand All @@ -33,38 +30,17 @@ function isAutoDismissable(n: Notification): boolean {
return n.level === 'error' && !!n.workflowId
}

function CountdownRing({ onPause }: { onPause: () => void }) {
function NotificationCountdownRing({ onPause }: { onPause: () => void }) {
return (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
onClick={onPause}
aria-label='Keep notifications visible'
className='!p-[4px] -m-[2px] shrink-0 rounded-[5px] hover:bg-[var(--surface-active)]'
className='!p-[4px] -m-[2px] shrink-0 rounded-[5px] text-[var(--text-icon)] hover:bg-[var(--surface-active)]'
>
<svg
width='14'
height='14'
viewBox='0 0 16 16'
fill='none'
xmlns='http://www.w3.org/2000/svg'
style={{ transform: 'rotate(-90deg) scaleX(-1)' }}
>
<circle cx='8' cy='8' r={RING_RADIUS} stroke='var(--border)' strokeWidth='1.5' />
<circle
cx='8'
cy='8'
r={RING_RADIUS}
stroke='var(--text-icon)'
strokeWidth='1.5'
strokeLinecap='round'
strokeDasharray={RING_CIRCUMFERENCE}
style={{
animation: `notification-countdown ${AUTO_DISMISS_MS}ms linear forwards`,
}}
/>
</svg>
<CountdownRing duration={AUTO_DISMISS_MS} />
</Button>
</Tooltip.Trigger>
<Tooltip.Content>
Expand Down Expand Up @@ -266,7 +242,7 @@ export const Notifications = memo(function Notifications({ embedded }: Notificat
{notification.message}
</div>
<div className='flex shrink-0 items-start gap-[2px]'>
{showCountdown && <CountdownRing onPause={pauseAll} />}
{showCountdown && <NotificationCountdownRing onPause={pauseAll} />}
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
Expand Down
1 change: 1 addition & 0 deletions apps/sim/components/emcn/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,5 +144,6 @@ export {
} from './tag-input/tag-input'
export { Textarea } from './textarea/textarea'
export { TimePicker, type TimePickerProps, timePickerVariants } from './time-picker/time-picker'
export { CountdownRing } from './toast/countdown-ring'
export { ToastProvider, toast, useToast } from './toast/toast'
export { Tooltip } from './tooltip/tooltip'
37 changes: 37 additions & 0 deletions apps/sim/components/emcn/components/toast/countdown-ring.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const RING_RADIUS = 5.5
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS

interface CountdownRingProps {
duration: number
paused?: boolean
className?: string
}

export function CountdownRing({ duration, paused = false, className }: CountdownRingProps) {
return (
<svg
width='14'
height='14'
viewBox='0 0 16 16'
fill='none'
xmlns='http://www.w3.org/2000/svg'
className={className}
style={{ transform: 'rotate(-90deg) scaleX(-1)' }}
>
<circle cx='8' cy='8' r={RING_RADIUS} stroke='currentColor' strokeWidth='1.5' opacity={0.2} />
<circle
cx='8'
cy='8'
r={RING_RADIUS}
stroke='currentColor'
strokeWidth='1.5'
strokeLinecap='round'
strokeDasharray={RING_CIRCUMFERENCE}
style={{
animation: `notification-countdown ${duration}ms linear forwards`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shared CountdownRing depends on external CSS keyframe

Low Severity

The CountdownRing component is now exported from the shared emcn component library, but it references the CSS animation name notification-countdown which is only defined in apps/sim/app/_styles/globals.css. This creates a hidden coupling — any consumer of CountdownRing outside the app context (or if the keyframe is renamed) would get a static, non-animating ring with no indication of failure. A self-contained component would define its own keyframe or accept it as a prop.

Additional Locations (1)
Fix in Cursor Fix in Web

animationPlayState: paused ? 'paused' : 'running',
}}
/>
</svg>
)
}
Comment thread
TheodoreSpeaks marked this conversation as resolved.
Loading