Skip to content
Merged
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/docs/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4330,7 +4330,7 @@ export function PylonIcon(props: SVGProps<SVGSVGElement>) {
viewBox='0 0 26 26'
fill='none'
>
<g clip-path='url(#clip0_6559_17753)'>
<g clipPath='url(#clip0_6559_17753)'>
<path
d='M21.3437 4.1562C18.9827 1.79763 15.8424 0.5 12.5015 0.5C9.16056 0.5 6.02027 1.79763 3.66091 4.15455C1.29989 6.51147 0 9.64465 0 12.9798C0 16.3149 1.29989 19.448 3.66091 21.805C6.02193 24.1619 9.16222 25.4612 12.5031 25.4612C15.844 25.4612 18.9843 24.1635 21.3454 21.8066C23.7064 19.4497 25.0063 16.3165 25.0063 12.9814C25.0063 9.6463 23.7064 6.51312 21.3454 4.1562H21.3437ZM22.3949 12.9814C22.3949 17.927 18.7074 22.1227 13.8063 22.7699V3.1896C18.7074 3.83676 22.3949 8.0342 22.3949 12.9798V12.9814ZM4.8265 6.75643C6.43312 4.7835 8.68803 3.52063 11.1983 3.1896V6.75643H4.8265ZM11.1983 9.36162V11.6904H2.69428C2.79874 10.8926 3.00267 10.1097 3.2978 9.36162H11.1983ZM11.1983 14.2939V16.6227H3.30775C3.00931 15.8746 2.80371 15.0917 2.6976 14.2939H11.1983ZM11.1983 19.2279V22.7699C8.70129 22.4405 6.45302 21.1859 4.84805 19.2279H11.1983Z'
fill='#5B0EFF'
Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/docs/en/tools/zendesk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ With Zendesk in Sim, you can:
By leveraging Zendesk’s Sim integration, your automated workflows can seamlessly handle support ticket triage, user onboarding/offboarding, company management, and keep your support operations running smoothly. Whether you’re integrating support with product, CRM, or automation systems, Zendesk tools in Sim provide robust, programmatic control to power best-in-class support at scale.
{/* MANUAL-CONTENT-END */}


## Usage Instructions

Integrate Zendesk into the workflow. Can get tickets, get ticket, create ticket, create tickets bulk, update ticket, update tickets bulk, delete ticket, merge tickets, get users, get user, get current user, search users, create user, create users bulk, update user, update users bulk, delete user, get organizations, get organization, autocomplete organizations, create organization, create organizations bulk, update organization, delete organization, search, search count.
Expand Down
98 changes: 59 additions & 39 deletions apps/sim/app/api/chat/manage/[id]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ describe('Chat Edit API Route', () => {
const mockCreateErrorResponse = vi.fn()
const mockEncryptSecret = vi.fn()
const mockCheckChatAccess = vi.fn()
const mockGetSession = vi.fn()
const mockDeployWorkflow = vi.fn()

beforeEach(() => {
vi.resetModules()

// Set default return values
mockLimit.mockResolvedValue([])
mockSelect.mockReturnValue({ from: mockFrom })
mockFrom.mockReturnValue({ where: mockWhere })
mockWhere.mockReturnValue({ limit: mockLimit })
Expand All @@ -43,10 +45,6 @@ describe('Chat Edit API Route', () => {
chat: { id: 'id', identifier: 'identifier', userId: 'userId' },
}))

vi.doMock('@/lib/auth', () => ({
getSession: mockGetSession,
}))

vi.doMock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn().mockReturnValue({
info: vi.fn(),
Expand Down Expand Up @@ -86,6 +84,15 @@ describe('Chat Edit API Route', () => {
vi.doMock('@/app/api/chat/utils', () => ({
checkChatAccess: mockCheckChatAccess,
}))

mockDeployWorkflow.mockResolvedValue({ success: true, version: 1 })
vi.doMock('@/lib/workflows/db-helpers', () => ({
deployWorkflow: mockDeployWorkflow,
}))

vi.doMock('drizzle-orm', () => ({
eq: vi.fn((field, value) => ({ field, value, type: 'eq' })),
}))
})

afterEach(() => {
Expand All @@ -94,20 +101,25 @@ describe('Chat Edit API Route', () => {

describe('GET', () => {
it('should return 401 when user is not authenticated', async () => {
mockGetSession.mockResolvedValueOnce(null)
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue(null),
}))

const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123')
const { GET } = await import('@/app/api/chat/manage/[id]/route')
const response = await GET(req, { params: Promise.resolve({ id: 'chat-123' }) })

expect(response.status).toBe(401)
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unauthorized', 401)
const data = await response.json()
expect(data.error).toBe('Unauthorized')
})

it('should return 404 when chat not found or access denied', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-id' },
})
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-id' },
}),
}))

mockCheckChatAccess.mockResolvedValue({ hasAccess: false })

Expand All @@ -116,7 +128,8 @@ describe('Chat Edit API Route', () => {
const response = await GET(req, { params: Promise.resolve({ id: 'chat-123' }) })

expect(response.status).toBe(404)
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Chat not found or access denied', 404)
const data = await response.json()
expect(data.error).toBe('Chat not found or access denied')
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'user-id')
})

Expand All @@ -143,15 +156,12 @@ describe('Chat Edit API Route', () => {
const response = await GET(req, { params: Promise.resolve({ id: 'chat-123' }) })

expect(response.status).toBe(200)
expect(mockCreateSuccessResponse).toHaveBeenCalledWith({
id: 'chat-123',
identifier: 'test-chat',
title: 'Test Chat',
description: 'A test chat',
customizations: { primaryColor: '#000000' },
chatUrl: 'http://localhost:3000/chat/test-chat',
hasPassword: true,
})
const data = await response.json()
expect(data.id).toBe('chat-123')
expect(data.identifier).toBe('test-chat')
expect(data.title).toBe('Test Chat')
expect(data.chatUrl).toBe('http://localhost:3000/chat/test-chat')
expect(data.hasPassword).toBe(true)
})
})

Expand All @@ -169,7 +179,8 @@ describe('Chat Edit API Route', () => {
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })

expect(response.status).toBe(401)
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unauthorized', 401)
const data = await response.json()
expect(data.error).toBe('Unauthorized')
})

it('should return 404 when chat not found or access denied', async () => {
Expand All @@ -189,7 +200,8 @@ describe('Chat Edit API Route', () => {
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })

expect(response.status).toBe(404)
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Chat not found or access denied', 404)
const data = await response.json()
expect(data.error).toBe('Chat not found or access denied')
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'user-id')
})

Expand All @@ -205,9 +217,11 @@ describe('Chat Edit API Route', () => {
identifier: 'test-chat',
title: 'Test Chat',
authType: 'public',
workflowId: 'workflow-123',
}

mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
mockLimit.mockResolvedValueOnce([]) // No identifier conflict

const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
method: 'PATCH',
Expand All @@ -218,11 +232,10 @@ describe('Chat Edit API Route', () => {

expect(response.status).toBe(200)
expect(mockUpdate).toHaveBeenCalled()
expect(mockCreateSuccessResponse).toHaveBeenCalledWith({
id: 'chat-123',
chatUrl: 'http://localhost:3000/chat/test-chat',
message: 'Chat deployment updated successfully',
})
const data = await response.json()
expect(data.id).toBe('chat-123')
expect(data.chatUrl).toBe('http://localhost:3000/chat/test-chat')
expect(data.message).toBe('Chat deployment updated successfully')
})

it('should handle identifier conflicts', async () => {
Expand All @@ -236,11 +249,15 @@ describe('Chat Edit API Route', () => {
id: 'chat-123',
identifier: 'test-chat',
title: 'Test Chat',
workflowId: 'workflow-123',
}

mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
// Mock identifier conflict
mockLimit.mockResolvedValueOnce([{ id: 'other-chat-id', identifier: 'new-identifier' }])

// Reset and reconfigure mockLimit to return the conflict
mockLimit.mockReset()
mockLimit.mockResolvedValue([{ id: 'other-chat-id', identifier: 'new-identifier' }])
mockWhere.mockReturnValue({ limit: mockLimit })

const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
method: 'PATCH',
Expand All @@ -250,7 +267,8 @@ describe('Chat Edit API Route', () => {
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })

expect(response.status).toBe(400)
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Identifier already in use', 400)
const data = await response.json()
expect(data.error).toBe('Identifier already in use')
})

it('should validate password requirement for password auth', async () => {
Expand All @@ -266,6 +284,7 @@ describe('Chat Edit API Route', () => {
title: 'Test Chat',
authType: 'public',
password: null,
workflowId: 'workflow-123',
}

mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
Expand All @@ -278,10 +297,8 @@ describe('Chat Edit API Route', () => {
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })

expect(response.status).toBe(400)
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
'Password is required when using password protection',
400
)
const data = await response.json()
expect(data.error).toBe('Password is required when using password protection')
})

it('should allow access when user has workspace admin permission', async () => {
Expand All @@ -296,10 +313,12 @@ describe('Chat Edit API Route', () => {
identifier: 'test-chat',
title: 'Test Chat',
authType: 'public',
workflowId: 'workflow-123',
}

// User doesn't own chat but has workspace admin access
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
mockLimit.mockResolvedValueOnce([]) // No identifier conflict

const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
method: 'PATCH',
Expand All @@ -326,7 +345,8 @@ describe('Chat Edit API Route', () => {
const response = await DELETE(req, { params: Promise.resolve({ id: 'chat-123' }) })

expect(response.status).toBe(401)
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unauthorized', 401)
const data = await response.json()
expect(data.error).toBe('Unauthorized')
})

it('should return 404 when chat not found or access denied', async () => {
Expand All @@ -345,7 +365,8 @@ describe('Chat Edit API Route', () => {
const response = await DELETE(req, { params: Promise.resolve({ id: 'chat-123' }) })

expect(response.status).toBe(404)
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Chat not found or access denied', 404)
const data = await response.json()
expect(data.error).toBe('Chat not found or access denied')
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'user-id')
})

Expand All @@ -367,9 +388,8 @@ describe('Chat Edit API Route', () => {

expect(response.status).toBe(200)
expect(mockDelete).toHaveBeenCalled()
expect(mockCreateSuccessResponse).toHaveBeenCalledWith({
message: 'Chat deployment deleted successfully',
})
const data = await response.json()
expect(data.message).toBe('Chat deployment deleted successfully')
})

it('should allow deletion when user has workspace admin permission', async () => {
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/tools/custom/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ export async function POST(req: NextRequest) {
}
} catch (error) {
logger.error(`[${requestId}] Error updating custom tools`, error)
return NextResponse.json({ error: 'Failed to update custom tools' }, { status: 500 })
const errorMessage = error instanceof Error ? error.message : 'Failed to update custom tools'
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,9 @@ export function CodeEditor({
return () => resizeObserver.disconnect()
}, [code])

// Calculate the number of lines to determine gutter width
const lineCount = code.split('\n').length
const gutterWidth = calculateGutterWidth(lineCount)

// Render helpers
const renderLineNumbers = () => {
const numbers: ReactElement[] = []
let lineNumber = 1
Expand All @@ -127,88 +125,41 @@ export function CodeEditor({
return numbers
}

// Custom highlighter that highlights environment variables and tags
const customHighlight = (code: string) => {
if (!highlightVariables || language !== 'javascript') {
// Use default Prism highlighting for non-JS or when variable highlighting is off
return highlight(code, languages[language], language)
}

// First, get the default Prism highlighting
let highlighted = highlight(code, languages[language], language)
const placeholders: Array<{ placeholder: string; original: string; type: 'env' | 'param' }> = []
let processedCode = code

// Collect all syntax highlights to apply in a single pass
type SyntaxHighlight = {
start: number
end: number
replacement: string
}
const highlights: SyntaxHighlight[] = []

// Find environment variables with {{var_name}} syntax
let match
const envVarRegex = /\{\{([^}]+)\}\}/g
while ((match = envVarRegex.exec(highlighted)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
replacement: `<span class="text-[#34B5FF] dark:text-[#34B5FF]">${match[0]}</span>`,
})
}

// Find tags with <tag_name> syntax (not in HTML context)
if (!language.includes('html')) {
const tagRegex = /<([^>\s/]+)>/g
while ((match = tagRegex.exec(highlighted)) !== null) {
// Skip HTML comments and closing tags
if (!match[0].startsWith('<!--') && !match[0].includes('</')) {
const escaped = `&lt;${match[1]}&gt;`
highlights.push({
start: match.index,
end: match.index + match[0].length,
replacement: `<span class="text-[#34B5FF] dark:text-[#34B5FF]">${escaped}</span>`,
})
}
}
}
processedCode = processedCode.replace(/\{\{([^}]+)\}\}/g, (match) => {
const placeholder = `__ENV_VAR_${placeholders.length}__`
placeholders.push({ placeholder, original: match, type: 'env' })
return placeholder
})

// Find schema parameters as whole words
if (schemaParameters.length > 0) {
schemaParameters.forEach((param) => {
// Escape special regex characters in parameter name
const escapedName = param.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const paramRegex = new RegExp(`\\b(${escapedName})\\b`, 'g')
while ((match = paramRegex.exec(highlighted)) !== null) {
// Check if this position is already inside an HTML tag
// by looking for unclosed < before this position
let insideTag = false
let pos = match.index - 1
while (pos >= 0) {
if (highlighted[pos] === '>') break
if (highlighted[pos] === '<') {
insideTag = true
break
}
pos--
}

if (!insideTag) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
replacement: `<span class="text-[#34B5FF] dark:text-[#34B5FF] font-medium">${match[0]}</span>`,
})
}
}
processedCode = processedCode.replace(paramRegex, (match) => {
const placeholder = `__PARAM_${placeholders.length}__`
placeholders.push({ placeholder, original: match, type: 'param' })
return placeholder
})
})
}

// Sort highlights by start position (reverse order to maintain positions)
highlights.sort((a, b) => b.start - a.start)
let highlighted = highlight(processedCode, languages[language], language)

placeholders.forEach(({ placeholder, original, type }) => {
const replacement =
type === 'env'
? `<span style="color: #34B5FF;">${original}</span>`
: `<span style="color: #34B5FF; font-weight: 500;">${original}</span>`

// Apply all highlights
highlights.forEach(({ start, end, replacement }) => {
highlighted = highlighted.slice(0, start) + replacement + highlighted.slice(end)
highlighted = highlighted.replace(placeholder, replacement)
})

return highlighted
Expand Down
Loading