From 135c12b4954eb4ab4b23002ed92eadbb6c82ce46 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 19 Mar 2026 12:13:19 -0700 Subject: [PATCH 1/9] Enforce name uniqueness --- .../app/_shell/providers/get-query-client.ts | 2 +- apps/sim/app/api/knowledge/[id]/route.ts | 5 ++ apps/sim/app/api/knowledge/route.ts | 5 ++ apps/sim/app/api/table/[tableId]/route.ts | 5 ++ .../emcn/components/toast/toast.tsx | 75 +++++++++++++++++-- apps/sim/hooks/queries/kb/knowledge.ts | 4 + apps/sim/hooks/queries/tables.ts | 4 + apps/sim/hooks/queries/workspace-files.ts | 4 + apps/sim/lib/core/errors.ts | 9 +++ apps/sim/lib/core/utils/restore-name.ts | 35 +++++++++ apps/sim/lib/knowledge/service.ts | 68 ++++++++++++++++- apps/sim/lib/table/service.ts | 55 +++++++++++--- .../workspace/workspace-file-manager.ts | 11 ++- 13 files changed, 257 insertions(+), 25 deletions(-) create mode 100644 apps/sim/lib/core/errors.ts create mode 100644 apps/sim/lib/core/utils/restore-name.ts diff --git a/apps/sim/app/_shell/providers/get-query-client.ts b/apps/sim/app/_shell/providers/get-query-client.ts index 8d7d2ecbb79..41093221d1c 100644 --- a/apps/sim/app/_shell/providers/get-query-client.ts +++ b/apps/sim/app/_shell/providers/get-query-client.ts @@ -11,7 +11,7 @@ function makeQueryClient() { retryOnMount: false, }, mutations: { - retry: 1, + retry: false, }, dehydrate: { shouldDehydrateQuery: (query) => diff --git a/apps/sim/app/api/knowledge/[id]/route.ts b/apps/sim/app/api/knowledge/[id]/route.ts index 7c3075a5d8b..02344d65ee4 100644 --- a/apps/sim/app/api/knowledge/[id]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/route.ts @@ -5,6 +5,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' +import { DuplicateNameError } from '@/lib/core/errors' import { deleteKnowledgeBase, getKnowledgeBaseById, @@ -166,6 +167,10 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: throw validationError } } catch (error) { + if (error instanceof DuplicateNameError) { + 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 }) } diff --git a/apps/sim/app/api/knowledge/route.ts b/apps/sim/app/api/knowledge/route.ts index d6a80bab115..5f4fd75ccbc 100644 --- a/apps/sim/app/api/knowledge/route.ts +++ b/apps/sim/app/api/knowledge/route.ts @@ -5,6 +5,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' +import { DuplicateNameError } from '@/lib/core/errors' import { createKnowledgeBase, getKnowledgeBases, @@ -149,6 +150,10 @@ export async function POST(req: NextRequest) { throw validationError } } catch (error) { + if (error instanceof DuplicateNameError) { + 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 }) } diff --git a/apps/sim/app/api/table/[tableId]/route.ts b/apps/sim/app/api/table/[tableId]/route.ts index 2341c9f8ad1..7b8f13e5739 100644 --- a/apps/sim/app/api/table/[tableId]/route.ts +++ b/apps/sim/app/api/table/[tableId]/route.ts @@ -3,6 +3,7 @@ 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 { DuplicateNameError } from '@/lib/core/errors' import { deleteTable, NAME_PATTERN, renameTable, TABLE_LIMITS, type TableSchema } from '@/lib/table' import { accessError, checkAccess, normalizeColumn } from '@/app/api/table/utils' @@ -136,6 +137,10 @@ export async function PATCH(request: NextRequest, { params }: TableRouteParams) ) } + if (error instanceof DuplicateNameError) { + 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' }, diff --git a/apps/sim/components/emcn/components/toast/toast.tsx b/apps/sim/components/emcn/components/toast/toast.tsx index 965f6ede819..66651a234d6 100644 --- a/apps/sim/components/emcn/components/toast/toast.tsx +++ b/apps/sim/components/emcn/components/toast/toast.tsx @@ -18,6 +18,9 @@ const AUTO_DISMISS_MS = 0 const EXIT_ANIMATION_MS = 200 const MAX_VISIBLE = 20 +const RING_RADIUS = 5.5 +const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS + type ToastVariant = 'default' | 'success' | 'error' interface ToastAction { @@ -100,7 +103,10 @@ const VARIANT_STYLES: Record = { function ToastItem({ toast: t, onDismiss }: { toast: ToastData; onDismiss: (id: string) => void }) { const [exiting, setExiting] = useState(false) + const [paused, setPaused] = useState(false) const timerRef = useRef>(undefined) + const remainingRef = useRef(t.duration) + const startRef = useRef(0) const dismiss = useCallback(() => { setExiting(true) @@ -109,13 +115,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 (
)} - +
+ {hasDuration && ( + + + + + )} + +
) } diff --git a/apps/sim/hooks/queries/kb/knowledge.ts b/apps/sim/hooks/queries/kb/knowledge.ts index 00cad83762a..73d6b765d24 100644 --- a/apps/sim/hooks/queries/kb/knowledge.ts +++ b/apps/sim/hooks/queries/kb/knowledge.ts @@ -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, @@ -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), diff --git a/apps/sim/hooks/queries/tables.ts b/apps/sim/hooks/queries/tables.ts index 5cdf019b246..cef8e4447fc 100644 --- a/apps/sim/hooks/queries/tables.ts +++ b/apps/sim/hooks/queries/tables.ts @@ -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') @@ -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() }) diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index 224f074f7ad..2ac4d7b9d3e 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -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') @@ -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() }) }, diff --git a/apps/sim/lib/core/errors.ts b/apps/sim/lib/core/errors.ts new file mode 100644 index 00000000000..3d955284838 --- /dev/null +++ b/apps/sim/lib/core/errors.ts @@ -0,0 +1,9 @@ +/** + * Thrown when a create/rename operation would violate a workspace-scoped + * unique name constraint (e.g. tables, knowledge bases, files). + */ +export class DuplicateNameError extends Error { + constructor(entity: string, name: string) { + super(`A ${entity} named "${name}" already exists in this workspace`) + } +} diff --git a/apps/sim/lib/core/utils/restore-name.ts b/apps/sim/lib/core/utils/restore-name.ts new file mode 100644 index 00000000000..a4a15f3a5aa --- /dev/null +++ b/apps/sim/lib/core/utils/restore-name.ts @@ -0,0 +1,35 @@ +import { randomBytes } from 'crypto' + +/** + * Generates a unique name for a restored entity by trying in order: + * 1. The original name + * 2. `name_restored` (inserted before file extension when `hasExtension` is true) + * 3. `name_restored_{6-char hex}` (practically guaranteed unique) + */ +export async function generateRestoreName( + originalName: string, + nameExists: (name: string) => Promise, + options?: { hasExtension?: boolean } +): Promise { + if (!(await nameExists(originalName))) { + return originalName + } + + const restoredName = addSuffix(originalName, '_restored', options?.hasExtension) + if (!(await nameExists(restoredName))) { + return restoredName + } + + const hash = randomBytes(3).toString('hex') + return addSuffix(originalName, `_restored_${hash}`, options?.hasExtension) +} + +function addSuffix(name: string, suffix: string, hasExtension?: boolean): string { + if (hasExtension) { + const dotIndex = name.lastIndexOf('.') + if (dotIndex > 0) { + return `${name.slice(0, dotIndex)}${suffix}${name.slice(dotIndex)}` + } + } + return `${name}${suffix}` +} diff --git a/apps/sim/lib/knowledge/service.ts b/apps/sim/lib/knowledge/service.ts index 271d4934d0d..86f4b38ee7e 100644 --- a/apps/sim/lib/knowledge/service.ts +++ b/apps/sim/lib/knowledge/service.ts @@ -2,7 +2,9 @@ import { randomUUID } from 'crypto' import { db } from '@sim/db' import { document, knowledgeBase, knowledgeConnector, permissions, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, count, eq, inArray, isNotNull, isNull, or, sql } from 'drizzle-orm' +import { and, count, eq, inArray, isNotNull, isNull, ne, or, sql } from 'drizzle-orm' +import { DuplicateNameError } from '@/lib/core/errors' +import { generateRestoreName } from '@/lib/core/utils/restore-name' import type { ChunkingConfig, CreateKnowledgeBaseData, @@ -157,6 +159,22 @@ export async function createKnowledgeBase( deletedAt: null, } + const duplicate = await db + .select({ id: knowledgeBase.id }) + .from(knowledgeBase) + .where( + and( + eq(knowledgeBase.workspaceId, data.workspaceId), + eq(knowledgeBase.name, data.name), + isNull(knowledgeBase.deletedAt) + ) + ) + .limit(1) + + if (duplicate.length > 0) { + throw new DuplicateNameError('knowledge base', data.name) + } + await db.insert(knowledgeBase).values(newKnowledgeBase) logger.info(`[${requestId}] Created knowledge base: ${data.name} (${kbId})`) @@ -222,6 +240,33 @@ export async function updateKnowledgeBase( updateData.embeddingDimension = 1536 } + if (updates.name !== undefined) { + const existing = await db + .select({ id: knowledgeBase.id, workspaceId: knowledgeBase.workspaceId }) + .from(knowledgeBase) + .where(and(eq(knowledgeBase.id, knowledgeBaseId), isNull(knowledgeBase.deletedAt))) + .limit(1) + + if (existing.length > 0 && existing[0].workspaceId) { + const duplicate = await db + .select({ id: knowledgeBase.id }) + .from(knowledgeBase) + .where( + and( + eq(knowledgeBase.workspaceId, existing[0].workspaceId), + eq(knowledgeBase.name, updates.name), + isNull(knowledgeBase.deletedAt), + ne(knowledgeBase.id, knowledgeBaseId) + ) + ) + .limit(1) + + if (duplicate.length > 0) { + throw new DuplicateNameError('knowledge base', updates.name) + } + } + } + await db .update(knowledgeBase) .set(updateData) @@ -383,6 +428,7 @@ export async function restoreKnowledgeBase( const [kb] = await db .select({ id: knowledgeBase.id, + name: knowledgeBase.name, deletedAt: knowledgeBase.deletedAt, workspaceId: knowledgeBase.workspaceId, }) @@ -406,6 +452,22 @@ export async function restoreKnowledgeBase( } } + const newName = await generateRestoreName(kb.name, async (candidate) => { + if (!kb.workspaceId) return false + const [match] = await db + .select({ id: knowledgeBase.id }) + .from(knowledgeBase) + .where( + and( + eq(knowledgeBase.workspaceId, kb.workspaceId), + eq(knowledgeBase.name, candidate), + isNull(knowledgeBase.deletedAt) + ) + ) + .limit(1) + return !!match + }) + const now = new Date() await db.transaction(async (tx) => { @@ -413,7 +475,7 @@ export async function restoreKnowledgeBase( await tx .update(knowledgeBase) - .set({ deletedAt: null, updatedAt: now }) + .set({ deletedAt: null, updatedAt: now, name: newName }) .where(eq(knowledgeBase.id, knowledgeBaseId)) await tx @@ -439,5 +501,5 @@ export async function restoreKnowledgeBase( ) }) - logger.info(`[${requestId}] Restored knowledge base: ${knowledgeBaseId}`) + logger.info(`[${requestId}] Restored knowledge base: ${knowledgeBaseId} as "${newName}"`) } diff --git a/apps/sim/lib/table/service.ts b/apps/sim/lib/table/service.ts index c00fac9856a..023c2d8ae96 100644 --- a/apps/sim/lib/table/service.ts +++ b/apps/sim/lib/table/service.ts @@ -11,6 +11,8 @@ import { db } from '@sim/db' import { userTableDefinitions, userTableRows } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, count, eq, gt, gte, inArray, isNull, sql } from 'drizzle-orm' +import { DuplicateNameError } from '@/lib/core/errors' +import { generateRestoreName } from '@/lib/core/utils/restore-name' import { COLUMN_TYPES, NAME_PATTERN, TABLE_LIMITS, USER_TABLE_ROWS_SQL_NAME } from './constants' import { buildFilterClause, buildSortClause } from './sql' import type { @@ -394,18 +396,32 @@ export async function renameTable( } const now = new Date() - const result = await db - .update(userTableDefinitions) - .set({ name: newName, updatedAt: now }) - .where(eq(userTableDefinitions.id, tableId)) - .returning({ id: userTableDefinitions.id }) + try { + const result = await db + .update(userTableDefinitions) + .set({ name: newName, updatedAt: now }) + .where(eq(userTableDefinitions.id, tableId)) + .returning({ id: userTableDefinitions.id }) - if (result.length === 0) { - throw new Error(`Table ${tableId} not found`) - } + if (result.length === 0) { + throw new Error(`Table ${tableId} not found`) + } - logger.info(`[${requestId}] Renamed table ${tableId} to "${newName}"`) - return { id: tableId, name: newName } + logger.info(`[${requestId}] Renamed table ${tableId} to "${newName}"`) + return { id: tableId, name: newName } + } catch (error: unknown) { + if ( + error instanceof Error && + 'cause' in error && + typeof error.cause === 'object' && + error.cause !== null && + 'code' in error.cause && + error.cause.code === '23505' + ) { + throw new DuplicateNameError('table', newName) + } + throw error + } } /** @@ -468,12 +484,27 @@ export async function restoreTable(tableId: string, requestId: string): Promise< } } + const newName = await generateRestoreName(table.name, async (candidate) => { + const [match] = await db + .select({ id: userTableDefinitions.id }) + .from(userTableDefinitions) + .where( + and( + eq(userTableDefinitions.workspaceId, table.workspaceId), + eq(userTableDefinitions.name, candidate), + isNull(userTableDefinitions.archivedAt) + ) + ) + .limit(1) + return !!match + }) + await db .update(userTableDefinitions) - .set({ archivedAt: null, updatedAt: new Date() }) + .set({ archivedAt: null, updatedAt: new Date(), name: newName }) .where(eq(userTableDefinitions.id, tableId)) - logger.info(`[${requestId}] Restored table ${tableId}`) + logger.info(`[${requestId}] Restored table ${tableId} as "${newName}"`) } /** diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index 4d18638d3ff..00ec4c898d4 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -18,6 +18,7 @@ import { uploadFile, } from '@/lib/uploads/core/storage-service' import { getFileMetadataByKey, insertFileMetadata } from '@/lib/uploads/server/metadata' +import { generateRestoreName } from '@/lib/core/utils/restore-name' import { isUuid, sanitizeFileName } from '@/executor/constants' import type { UserFile } from '@/executor/types' @@ -591,9 +592,15 @@ export async function restoreWorkspaceFile(workspaceId: string, fileId: string): throw new Error('Cannot restore file into an archived workspace') } + const newName = await generateRestoreName( + fileRecord.name, + (candidate) => fileExistsInWorkspace(workspaceId, candidate), + { hasExtension: true } + ) + await db .update(workspaceFiles) - .set({ deletedAt: null }) + .set({ deletedAt: null, originalName: newName }) .where( and( eq(workspaceFiles.id, fileId), @@ -602,5 +609,5 @@ export async function restoreWorkspaceFile(workspaceId: string, fileId: string): ) ) - logger.info(`Successfully restored workspace file: ${fileRecord.name}`) + logger.info(`Successfully restored workspace file: ${newName}`) } From 0c8158c6f851a6f1bfde425e71e4392d73adea89 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 19 Mar 2026 12:34:44 -0700 Subject: [PATCH 2/9] Use established pattern for error handling --- apps/sim/app/api/knowledge/[id]/route.ts | 4 +- apps/sim/app/api/knowledge/route.ts | 4 +- apps/sim/app/api/table/[tableId]/route.ts | 12 +++-- .../notifications/notifications.tsx | 33 +++----------- .../emcn/components/toast/countdown-ring.tsx | 44 +++++++++++++++++++ .../emcn/components/toast/toast.tsx | 38 +--------------- apps/sim/lib/core/config/feature-flags.ts | 6 +-- apps/sim/lib/core/errors.ts | 9 ---- apps/sim/lib/knowledge/service.ts | 12 +++-- apps/sim/lib/table/service.ts | 10 ++++- 10 files changed, 84 insertions(+), 88 deletions(-) create mode 100644 apps/sim/components/emcn/components/toast/countdown-ring.tsx delete mode 100644 apps/sim/lib/core/errors.ts diff --git a/apps/sim/app/api/knowledge/[id]/route.ts b/apps/sim/app/api/knowledge/[id]/route.ts index 02344d65ee4..2dcf53701da 100644 --- a/apps/sim/app/api/knowledge/[id]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/route.ts @@ -5,10 +5,10 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' -import { DuplicateNameError } from '@/lib/core/errors' import { deleteKnowledgeBase, getKnowledgeBaseById, + KnowledgeBaseConflictError, updateKnowledgeBase, } from '@/lib/knowledge/service' import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils' @@ -167,7 +167,7 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: throw validationError } } catch (error) { - if (error instanceof DuplicateNameError) { + if (error instanceof KnowledgeBaseConflictError) { return NextResponse.json({ error: error.message }, { status: 409 }) } diff --git a/apps/sim/app/api/knowledge/route.ts b/apps/sim/app/api/knowledge/route.ts index 5f4fd75ccbc..28fe86ef016 100644 --- a/apps/sim/app/api/knowledge/route.ts +++ b/apps/sim/app/api/knowledge/route.ts @@ -5,10 +5,10 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' -import { DuplicateNameError } from '@/lib/core/errors' import { createKnowledgeBase, getKnowledgeBases, + KnowledgeBaseConflictError, type KnowledgeBaseScope, } from '@/lib/knowledge/service' @@ -150,7 +150,7 @@ export async function POST(req: NextRequest) { throw validationError } } catch (error) { - if (error instanceof DuplicateNameError) { + if (error instanceof KnowledgeBaseConflictError) { return NextResponse.json({ error: error.message }, { status: 409 }) } diff --git a/apps/sim/app/api/table/[tableId]/route.ts b/apps/sim/app/api/table/[tableId]/route.ts index 7b8f13e5739..30a99c951b3 100644 --- a/apps/sim/app/api/table/[tableId]/route.ts +++ b/apps/sim/app/api/table/[tableId]/route.ts @@ -3,8 +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 { DuplicateNameError } from '@/lib/core/errors' -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') @@ -137,7 +143,7 @@ export async function PATCH(request: NextRequest, { params }: TableRouteParams) ) } - if (error instanceof DuplicateNameError) { + if (error instanceof TableConflictError) { return NextResponse.json({ error: error.message }, { status: 409 }) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx index 834d83054f2..58f355abeec 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx @@ -2,6 +2,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 { CountdownRing } from '@/components/emcn/components/toast/countdown-ring' 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' @@ -20,9 +21,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 = { copilot: 'Fix in Copilot', refresh: 'Refresh', @@ -33,7 +31,7 @@ function isAutoDismissable(n: Notification): boolean { return n.level === 'error' && !!n.workflowId } -function CountdownRing({ onPause }: { onPause: () => void }) { +function NotificationCountdownRing({ onPause }: { onPause: () => void }) { return ( @@ -41,30 +39,9 @@ function CountdownRing({ onPause }: { onPause: () => void }) { 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)]' > - - - - + @@ -266,7 +243,7 @@ export const Notifications = memo(function Notifications({ embedded }: Notificat {notification.message}
- {showCountdown && } + {showCountdown && } )}
- {hasDuration && ( - - - - - )} + {hasDuration && }