Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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: 2 additions & 0 deletions apps/docs/content/docs/en/tools/servicenow.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ Read records from a ServiceNow table
| `number` | string | No | Record number \(e.g., INC0010001\) |
| `query` | string | No | Encoded query string \(e.g., "active=true^priority=1"\) |
| `limit` | number | No | Maximum number of records to return \(e.g., 10, 50, 100\) |
| `offset` | number | No | Number of records to skip for pagination \(e.g., 0, 10, 20\) |
| `fields` | string | No | Comma-separated list of fields to return \(e.g., sys_id,number,short_description,state\) |
| `displayValue` | string | No | Return display values for reference fields: "true" \(display only\), "false" \(sys_id only\), or "all" \(both\) |

#### Output

Expand Down
28 changes: 26 additions & 2 deletions apps/docs/content/docs/en/tools/slack.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Slack
description: Send, update, delete messages, send ephemeral messages, add reactions in Slack or trigger workflows from Slack events
description: Send, update, delete messages, send ephemeral messages, add or remove reactions in Slack or trigger workflows from Slack events
---

import { BlockInfoCard } from "@/components/ui/block-info-card"
Expand Down Expand Up @@ -39,7 +39,7 @@ If you encounter issues with the Slack integration, contact us at [help@sim.ai](

## Usage Instructions

Integrate Slack into the workflow. Can send, update, and delete messages, send ephemeral messages visible only to a specific user, create canvases, read messages, and add reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.
Integrate Slack into the workflow. Can send, update, and delete messages, send ephemeral messages visible only to a specific user, create canvases, read messages, and add or remove reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.



Expand Down Expand Up @@ -799,4 +799,28 @@ Add an emoji reaction to a Slack message
| ↳ `timestamp` | string | Message timestamp |
| ↳ `reaction` | string | Emoji reaction name |

### `slack_remove_reaction`

Remove an emoji reaction from a Slack message

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `channel` | string | Yes | Channel ID where the message was posted \(e.g., C1234567890\) |
| `timestamp` | string | Yes | Timestamp of the message to remove reaction from \(e.g., 1405894322.002768\) |
| `name` | string | Yes | Name of the emoji reaction to remove \(without colons, e.g., thumbsup, heart, eyes\) |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Success message |
| `metadata` | object | Reaction metadata |
| ↳ `channel` | string | Channel ID |
| ↳ `timestamp` | string | Message timestamp |
| ↳ `reaction` | string | Emoji reaction name |


87 changes: 87 additions & 0 deletions apps/sim/app/api/tools/slack/remove-reaction/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'

export const dynamic = 'force-dynamic'

const SlackRemoveReactionSchema = z.object({
accessToken: z.string().min(1, 'Access token is required'),
channel: z.string().min(1, 'Channel is required'),
timestamp: z.string().min(1, 'Message timestamp is required'),
name: z.string().min(1, 'Emoji name is required'),
})

export async function POST(request: NextRequest) {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })

if (!authResult.success) {
return NextResponse.json(
{
success: false,
error: authResult.error || 'Authentication required',
},
{ status: 401 }
)
}

const body = await request.json()
const validatedData = SlackRemoveReactionSchema.parse(body)

const slackResponse = await fetch('https://slack.com/api/reactions.remove', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${validatedData.accessToken}`,
},
body: JSON.stringify({
channel: validatedData.channel,
timestamp: validatedData.timestamp,
name: validatedData.name,
}),
})

const data = await slackResponse.json()

if (!data.ok) {
return NextResponse.json(
{
success: false,
error: data.error || 'Failed to remove reaction',
},
{ status: slackResponse.status }
)
Comment thread
waleedlatif1 marked this conversation as resolved.
}

return NextResponse.json({
success: true,
output: {
content: `Successfully removed :${validatedData.name}: reaction`,
metadata: {
channel: validatedData.channel,
timestamp: validatedData.timestamp,
reaction: validatedData.name,
},
},
})
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{
success: false,
error: 'Invalid request data',
details: error.errors,
},
{ status: 400 }
)
}

return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
},
{ status: 500 }
)
}
}
1 change: 1 addition & 0 deletions apps/sim/app/api/tools/stt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export async function POST(request: NextRequest) {
method: 'GET',
})
if (!response.ok) {
await response.text().catch(() => {})
throw new Error(`Failed to download audio from URL: ${response.statusText}`)
}

Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/tools/textract/parse/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ async function fetchDocumentBytes(url: string): Promise<{ bytes: string; content
method: 'GET',
})
if (!response.ok) {
await response.text().catch(() => {})
throw new Error(`Failed to fetch document: ${response.statusText}`)
}

Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/tools/tts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export async function POST(request: NextRequest) {
})

if (!response.ok) {
await response.body?.cancel().catch(() => {})
logger.error(`Failed to generate TTS: ${response.status} ${response.statusText}`)
return NextResponse.json(
{ error: `Failed to generate TTS: ${response.status} ${response.statusText}` },
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/tools/vision/analyze/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export async function POST(request: NextRequest) {
method: 'GET',
})
if (!response.ok) {
await response.text().catch(() => {})
return NextResponse.json(
{ success: false, error: 'Failed to fetch image for Gemini' },
{ status: 400 }
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/workflows/[id]/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -964,7 +964,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
logger.error(`[${requestId}] Error streaming block content:`, error)
} finally {
try {
reader.releaseLock()
await reader.cancel().catch(() => {})
} catch {}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,17 +501,6 @@ export function Chat() {
}
}, [])

useEffect(() => {
if (!isExecuting && isStreaming) {
const lastMessage = workflowMessages[workflowMessages.length - 1]
if (lastMessage?.isStreaming) {
streamReaderRef.current?.cancel()
streamReaderRef.current = null
finalizeMessageStream(lastMessage.id)
}
}
}, [isExecuting, isStreaming, workflowMessages, finalizeMessageStream])

const handleStopStreaming = useCallback(() => {
streamReaderRef.current?.cancel()
streamReaderRef.current = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/component
import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config'
import { getSubBlockStableKey } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/utils'
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import {
isAncestorProtected,
isBlockProtected,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils'
import { PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview'
import { getBlock } from '@/blocks/registry'
import type { SubBlockType } from '@/blocks/types'
Expand Down Expand Up @@ -107,12 +111,11 @@ export function Editor() {

const userPermissions = useUserPermissionsContext()

// Check if block is locked (or inside a locked container) and compute edit permission
// Check if block is locked (or inside a locked ancestor) and compute edit permission
// Locked blocks cannot be edited by anyone (admins can only lock/unlock)
const blocks = useWorkflowStore((state) => state.blocks)
const parentId = currentBlock?.data?.parentId as string | undefined
const isParentLocked = parentId ? (blocks[parentId]?.locked ?? false) : false
const isLocked = (currentBlock?.locked ?? false) || isParentLocked
const isLocked = currentBlockId ? isBlockProtected(currentBlockId, blocks) : false
const isAncestorLocked = currentBlockId ? isAncestorProtected(currentBlockId, blocks) : false
const canEditBlock = userPermissions.canEdit && !isLocked

const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
Expand Down Expand Up @@ -247,10 +250,7 @@ export function Editor() {
const block = blocks[blockId]
if (!block) return

const parentId = block.data?.parentId as string | undefined
const isParentLocked = parentId ? (blocks[parentId]?.locked ?? false) : false
const isLocked = (block.locked ?? false) || isParentLocked
if (!userPermissions.canEdit || isLocked) return
if (!userPermissions.canEdit || isBlockProtected(blockId, blocks)) return

renamingBlockIdRef.current = blockId
setEditedName(block.name || '')
Expand Down Expand Up @@ -364,11 +364,11 @@ export function Editor() {
)}
</div>
<div className='flex shrink-0 items-center gap-[8px]'>
{/* Locked indicator - clickable to unlock if user has admin permissions, block is locked, and parent is not locked */}
{/* Locked indicator - clickable to unlock if user has admin permissions, block is locked directly, and not locked by an ancestor */}
{isLocked && currentBlock && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
{userPermissions.canAdmin && currentBlock.locked && !isParentLocked ? (
{userPermissions.canAdmin && currentBlock.locked && !isAncestorLocked ? (
<Button
variant='ghost'
className='p-0'
Expand All @@ -385,8 +385,8 @@ export function Editor() {
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>
{isParentLocked
? 'Parent container is locked'
{isAncestorLocked
? 'Ancestor container is locked'
: userPermissions.canAdmin && currentBlock.locked
? 'Unlock block'
: 'Block is locked'}
Expand Down
Loading