Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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`,
animationPlayState: paused ? 'paused' : 'running',
}}
/>
</svg>
)
}
41 changes: 34 additions & 7 deletions apps/sim/components/emcn/components/toast/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { X } from 'lucide-react'
import { createPortal } from 'react-dom'
import { cn } from '@/lib/core/utils/cn'
import { CountdownRing } from './countdown-ring'

const AUTO_DISMISS_MS = 0
const EXIT_ANIMATION_MS = 200
Expand Down Expand Up @@ -100,7 +101,10 @@ const VARIANT_STYLES: Record<ToastVariant, string> = {

function ToastItem({ toast: t, onDismiss }: { toast: ToastData; onDismiss: (id: string) => void }) {
const [exiting, setExiting] = useState(false)
const [paused, setPaused] = useState(false)
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined)
const remainingRef = useRef(t.duration)
const startRef = useRef(0)

const dismiss = useCallback(() => {
setExiting(true)
Expand All @@ -109,13 +113,33 @@ function ToastItem({ toast: t, onDismiss }: { toast: ToastData; onDismiss: (id:

useEffect(() => {
if (t.duration > 0) {
startRef.current = Date.now()
remainingRef.current = t.duration
timerRef.current = setTimeout(dismiss, t.duration)
return () => clearTimeout(timerRef.current)
}
}, [dismiss, t.duration])

const handleMouseEnter = useCallback(() => {
if (t.duration <= 0) return
clearTimeout(timerRef.current)
remainingRef.current -= Date.now() - startRef.current
setPaused(true)
}, [t.duration])

const handleMouseLeave = useCallback(() => {
if (t.duration <= 0) return
setPaused(false)
startRef.current = Date.now()
timerRef.current = setTimeout(dismiss, Math.max(remainingRef.current, 0))
}, [dismiss, t.duration])

const hasDuration = t.duration > 0

return (
<div
onMouseEnter={hasDuration ? handleMouseEnter : undefined}
onMouseLeave={hasDuration ? handleMouseLeave : undefined}
className={cn(
'pointer-events-auto flex w-[320px] items-start gap-[8px] rounded-[8px] border px-[12px] py-[10px] shadow-md transition-all',
VARIANT_STYLES[t.variant],
Expand All @@ -142,13 +166,16 @@ function ToastItem({ toast: t, onDismiss }: { toast: ToastData; onDismiss: (id:
{t.action.label}
</button>
)}
<button
type='button'
onClick={dismiss}
className='shrink-0 rounded-[4px] p-[2px] opacity-60 hover:opacity-100'
>
<X className='h-[14px] w-[14px]' />
</button>
<div className='flex shrink-0 items-center gap-[4px]'>
{hasDuration && <CountdownRing duration={t.duration} paused={paused} />}
<button
type='button'
onClick={dismiss}
className='shrink-0 rounded-[4px] p-[2px] opacity-60 hover:opacity-100'
>
<X className='h-[14px] w-[14px]' />
</button>
</div>
</div>
)
}
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/hooks/queries/kb/knowledge.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createLogger } from '@sim/logger'
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from '@/components/emcn'
import type {
ChunkData,
ChunksPagination,
Expand Down Expand Up @@ -773,6 +774,9 @@ export function useUpdateKnowledgeBase(workspaceId?: string) {

return useMutation({
mutationFn: updateKnowledgeBase,
onError: (error) => {
toast.error(error.message, { duration: 5000 })
},
onSuccess: (_, { knowledgeBaseId }) => {
queryClient.invalidateQueries({
queryKey: knowledgeKeys.detail(knowledgeBaseId),
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/hooks/queries/tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { createLogger } from '@sim/logger'
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from '@/components/emcn'
import type { Filter, RowData, Sort, TableDefinition, TableMetadata, TableRow } from '@/lib/table'

const logger = createLogger('TableQueries')
Expand Down Expand Up @@ -308,6 +309,9 @@ export function useRenameTable(workspaceId: string) {

return res.json()
},
onError: (error) => {
toast.error(error.message, { duration: 5000 })
},
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({ queryKey: tableKeys.detail(variables.tableId) })
queryClient.invalidateQueries({ queryKey: tableKeys.lists() })
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/hooks/queries/workspace-files.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createLogger } from '@sim/logger'
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from '@/components/emcn'
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'

const logger = createLogger('WorkspaceFilesQuery')
Expand Down Expand Up @@ -245,6 +246,9 @@ export function useRenameWorkspaceFile() {

return data
},
onError: (error) => {
toast.error(error.message, { duration: 5000 })
},
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.lists() })
},
Expand Down
Loading
Loading