diff --git a/AiChatWorkflowsAutomationApp.ts b/AiChatWorkflowsAutomationApp.ts index c2e2561..4bba93f 100644 --- a/AiChatWorkflowsAutomationApp.ts +++ b/AiChatWorkflowsAutomationApp.ts @@ -17,15 +17,15 @@ import { } from "@rocket.chat/apps-engine/definition/messages"; import { PostMessageSentToBotHandler } from "./handler/PostMessageSentToBotHandler"; import { ChatAutomationCreate } from "./slashCommands/ChatAutomationCreate"; -import { UIKitViewSubmitInteractionContext } from "@rocket.chat/apps-engine/definition/uikit"; +import { IUIKitResponse, UIKitBlockInteractionContext, UIKitViewSubmitInteractionContext } from "@rocket.chat/apps-engine/definition/uikit"; import { ExecuteViewSubmitHandler } from "./handler/ExecuteViewSubmitHandler"; import { PostMessageSentHandler } from "./handler/PostMessageSentHandler"; import { ChatAutomation } from "./slashCommands/ChatAutomation"; +import { ExecuteBlockActionHandler } from "./handler/ExecuteBlockActionHandler"; export class AiChatWorkflowsAutomationApp extends App - implements IPostMessageSentToBot, IPostMessageSent -{ + implements IPostMessageSentToBot, IPostMessageSent { constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) { super(info, logger, accessors); } @@ -46,7 +46,23 @@ export class AiChatWorkflowsAutomationApp modify ); } - + public async executeBlockActionHandler( + context: UIKitBlockInteractionContext, + read: IRead, + http: IHttp, + persistence: IPersistence, + modify: IModify + ): Promise { + const handler = new ExecuteBlockActionHandler( + this, + read, + http, + modify, + persistence, + context + ); + return await handler.handleActions(); + } public async executeViewSubmitHandler( context: UIKitViewSubmitInteractionContext, read: IRead, diff --git a/README.md b/README.md index 24ea37f..932e948 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,7 @@ You can create chat workflow automations using **two methods**: | Command | Description | |--------|-------------| | `/chat-automation ping` | Sends a hello message to your Direct Messages (DM) from the app | -| `/chat-automation list` | Displays a list of all created workflows | -| `/chat-automation delete ` | Deletes the workflow with the specified ID | -| `/chat-automation enable ` | Enables the workflow with the specified ID | -| `/chat-automation disable ` | Disables the workflow with the specified ID | -| `/chat-automation notification off ` | Disables notifications for a workflow trigger | -| `/chat-automation notification on ` | Enables notifications for a workflow trigger | +| `/chat-automation list` | Displays a list of all created workflows where u can delete and edit them as well | --- diff --git a/definitions/MessageEnum.ts b/definitions/MessageEnum.ts index be07b77..feecad0 100644 --- a/definitions/MessageEnum.ts +++ b/definitions/MessageEnum.ts @@ -1,22 +1,17 @@ export enum MessageEnum { - SUCCESS_MESSAGE_APP_DM_DIRECT = `_Success! The Chat Automation workflow has been created._ + SUCCESS_MESSAGE_APP_DM_DIRECT = `_Success! The Chat Automation workflow has been created._ _For more details, please open the thread._`, - SUCCESS_MESSAGE_APP_DM_REASONING = `_Success! The Chat Automation workflow has been created._ + SUCCESS_MESSAGE_APP_DM_REASONING = `_Success! The Chat Automation workflow has been created._ _Here are the details:_`, SUCCESS_MESSAGE_UI_MODAL = `_Success! The Chat Automation workflow has been created._ - Automation command: + Automation command: `, - CONTINUE_IN_THREAD_MESSAGE = `For the current command, please continue the conversation in this thread. + CONTINUE_IN_THREAD_MESSAGE = `For the current command, please continue the conversation in this thread. To create a new command, start a new message - do not reply in this thread.`, WORKFLOW_NOT_FOUND_DM = `_No automation workflows found that were created using Chat. Please create a workflow using Chat first._`, WORKFLOW_NOT_FOUND_UI = `_No automation workflows found that were created using the UI Block. Please create a workflow using the UI Block first._`, CHAT_AUTOMATION_COMMAND_INSTRUCTION_MESSAGE = `πŸ› οΈ *Available Slash Commands*\n\n` + - `β€’ \`/chat-automation ping\` – Sends a hello message to your DM\n` + - `β€’ \`/chat-automation list\` – Lists all created workflows\n` + - `β€’ \`/chat-automation delete \` – Deletes the workflow with the specified ID\n` + - `β€’ \`/chat-automation enable \` – Enables the workflow with the specified ID\n` + - `β€’ \`/chat-automation disable \` – Disables the workflow with the specified ID\n` + - `β€’ \`/chat-automation notification on \` – Enables notifications for a workflow\n` + - `β€’ \`/chat-automation notification off \` – Disables notifications for a workflow\n\n` + - `πŸ‘‰ *Please provide a filter or action*, e.g., \`ping\`, \`list\`, \`delete workflowId_1753828680384\`, \`notification off workflowId_1753828680384\``, + `β€’ \`/chat-automation ping\` – Sends a hello message to your DM\n` + + `β€’ \`/chat-automation list\` – Lists all created workflows View, search, edit, and delete workflows \n` + + `πŸ‘‰ *Please provide a filter or action*, e.g., \`ping\`, \`list\`` } diff --git a/definitions/ModalsEnum.ts b/definitions/ModalsEnum.ts index 53a2831..ec4ad10 100644 --- a/definitions/ModalsEnum.ts +++ b/definitions/ModalsEnum.ts @@ -1,3 +1,4 @@ export enum Modals { AutomationCreate = "automation-create-modal", + ContextualBar = "ContextualBar-modal" } diff --git a/definitions/ModalsEnum/DeleteWorkflowModal.ts b/definitions/ModalsEnum/DeleteWorkflowModal.ts new file mode 100644 index 0000000..bc7d276 --- /dev/null +++ b/definitions/ModalsEnum/DeleteWorkflowModal.ts @@ -0,0 +1,7 @@ +export enum DeleteWorkflowModalEnum { + VIEW_ID = 'delete_workflow_modal', + WORKFLOW_ID_BLOCK = 'workflow_id_block', + CONFIRM_BLOCK = 'confirm_delete_block', + CANCEL_ACTION = 'cancel_delete_action', + SUBMIT_ACTION = 'confirm_delete_submit', +} diff --git a/definitions/ModalsEnum/Editworkflow.ts b/definitions/ModalsEnum/Editworkflow.ts new file mode 100644 index 0000000..9af585f --- /dev/null +++ b/definitions/ModalsEnum/Editworkflow.ts @@ -0,0 +1,17 @@ +export enum EditWorkflowModalEnum { + VIEW_ID = 'edit_workflow_modal', + USERS_BLOCK = 'edit_users_block', + USERS_ACTION = 'users', + CHANNELS_BLOCK = 'edit_channels_block', + CHANNELS_ACTION = 'channels', + CONDITION_BLOCK = 'edit_condition_block', + CONDITION_ACTION = 'condition', + ACTION_BLOCK = 'edit_action_block', + ACTION_ACTION = 'action', + RESPONSE_BLOCK = 'edit_response_block', + RESPONSE_ACTION = 'response', + NOTIFY_BLOCK = 'edit_notify_block', + NOTIFY_ACTION = 'notify', + ACTIVE_BLOCK = 'edit_active_block', + ACTIVE_ACTION = 'active', +} diff --git a/definitions/ModalsEnum/ListContextualBar.ts b/definitions/ModalsEnum/ListContextualBar.ts new file mode 100644 index 0000000..e9c57a3 --- /dev/null +++ b/definitions/ModalsEnum/ListContextualBar.ts @@ -0,0 +1,24 @@ +export enum WorkflowContextualBarEnum { + VIEW_ID = 'workflow_list_contextual_bar', + SEARCH_BLOCK_ID = 'workflow_search_block', + SEARCH_ACTION_ID = 'workflow_search_action', + CLOSE_ACTION_ID = 'workflow_close_action', + CLOSE_BLOCK_ID = 'workflow_close_block', + OVERFLOW_ACTION_ID = 'workflow_overflow_action', + EDIT_ACTION_VALUE = 'edit', + DELETE_ACTION_VALUE = 'delete', +} + +export enum ModalsAction { + dispatchActionConfigOnInput = 'on_character_entered', + dispatchActionConfigOnSelect = 'on_item_selected', +} + +export interface IWorkflow { + id: string; + command: string; + toNotify: boolean; + isActive: boolean; + createdViaUI: boolean; +} + diff --git a/handler/ExecuteBlockActionHandler.ts b/handler/ExecuteBlockActionHandler.ts new file mode 100644 index 0000000..c4d6f76 --- /dev/null +++ b/handler/ExecuteBlockActionHandler.ts @@ -0,0 +1,107 @@ +import { + IHttp, + IModify, + IPersistence, + IRead, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { + UIKitBlockInteractionContext, + IUIKitResponse, +} from "@rocket.chat/apps-engine/definition/uikit"; +import { AiChatWorkflowsAutomationApp } from "../AiChatWorkflowsAutomationApp"; +import { getTriggerResponse } from "../utils/PersistenceMethods"; +import { WorkflowContextualBarExecutor } from "./ExecuteContextBar"; +import { createDeleteWorkflowModal } from "../modals/DeleteModal"; +import { createEditWorkflowModal } from "../modals/EditModal"; +import { WorkflowContextualBarEnum } from "../definitions/ModalsEnum/ListContextualBar"; + +export class ExecuteBlockActionHandler { + private context: UIKitBlockInteractionContext; + + constructor( + private readonly app: AiChatWorkflowsAutomationApp, + private readonly read: IRead, + private readonly http: IHttp, + private readonly modify: IModify, + private readonly persistence: IPersistence, + context: UIKitBlockInteractionContext, + ) { + this.context = context; + } + + public async handleActions(): Promise { + const { actionId } = this.context.getInteractionData(); + + switch (true) { + // Search and close actions + case actionId === WorkflowContextualBarEnum.SEARCH_ACTION_ID: + case actionId === WorkflowContextualBarEnum.CLOSE_ACTION_ID: + return await this.handleContextualBarActions(); + case actionId?.startsWith(WorkflowContextualBarEnum.OVERFLOW_ACTION_ID): + return await this.handleOverflowAction(); + default: + return this.context.getInteractionResponder().successResponse(); + } + } + + private async handleContextualBarActions(): Promise { + const executor = new WorkflowContextualBarExecutor( + this.app, + this.read, + this.modify, + this.persistence, + ); + return await executor.handleActions(this.context); + } + + private async handleOverflowAction(): Promise { + const { value } = this.context.getInteractionData(); + + if (!value) { + return this.context.getInteractionResponder().errorResponse(); + } + + // Parse value format: "edit_workflowId_123" or "delete_workflowId_123" + const valueParts = value.split('_'); + const actionType = valueParts[0]; + const workflowId = valueParts.slice(1).join('_'); + + switch (actionType) { + case WorkflowContextualBarEnum.EDIT_ACTION_VALUE: + return await this.openEditModal(workflowId); + case WorkflowContextualBarEnum.DELETE_ACTION_VALUE: + return await this.openDeleteModal(workflowId); + default: + return this.context.getInteractionResponder().errorResponse(); + } + } + + private async openEditModal(workflowId: string): Promise { + try { + const workflow = await getTriggerResponse(this.read, workflowId); + + if (!workflow) { + console.error(`[openEditModal] Workflow not found: ${workflowId}`); + return this.context.getInteractionResponder().errorResponse(); + } + + const modal = await createEditWorkflowModal(workflow); + return this.context.getInteractionResponder().openModalViewResponse(modal); + + } catch (error) { + console.error(`[openEditModal] Error:`, error); + return this.context.getInteractionResponder().errorResponse(); + } + } + + private async openDeleteModal(workflowId: string): Promise { + try { + const workflow = await getTriggerResponse(this.read, workflowId); + const modal = await createDeleteWorkflowModal(workflowId, workflow?.command); + return this.context.getInteractionResponder().openModalViewResponse(modal); + } catch (error) { + console.error(`[openDeleteModal] Error:`, error); + return this.context.getInteractionResponder().errorResponse(); + } + } +} diff --git a/handler/ExecuteContextBar.ts b/handler/ExecuteContextBar.ts new file mode 100644 index 0000000..ba27db2 --- /dev/null +++ b/handler/ExecuteContextBar.ts @@ -0,0 +1,80 @@ +import { + IUIKitResponse, + UIKitBlockInteractionContext, +} from '@rocket.chat/apps-engine/definition/uikit'; +import { IRead, IModify, IPersistence } from '@rocket.chat/apps-engine/definition/accessors'; +import { listWorkflowContextualBar } from '../modals/ListContextualBar'; +import { findTriggerResponsesByCreatorAndLLM } from '../utils/PersistenceMethods'; +import { WorkflowContextualBarEnum } from '../definitions/ModalsEnum/ListContextualBar'; + +export class WorkflowContextualBarExecutor { + constructor( + private readonly app: any, + private readonly read: IRead, + private readonly modify: IModify, + private readonly persistence: IPersistence, + ) { } + + public async handleActions( + context: UIKitBlockInteractionContext, + ): Promise { + const data = context.getInteractionData(); + const { actionId, user, room, value } = data; + // Handle search input + if (actionId === WorkflowContextualBarEnum.SEARCH_ACTION_ID) { + + // Fetch workflows again + const chatWorkflows = await findTriggerResponsesByCreatorAndLLM( + this.read, + user.id, + true + ); + + const uiWorkflows = await findTriggerResponsesByCreatorAndLLM( + this.read, + user.id, + false + ); + + const transformedChatWorkflows = chatWorkflows.map((cmd) => ({ + id: cmd.data.id, + command: cmd.data.command, + toNotify: cmd.data.toNotify, + isActive: cmd.data.isActive, + createdViaUI: false, + })); + + const transformedUIWorkflows = uiWorkflows.map((cmd) => ({ + id: cmd.data.id, + command: cmd.data.command, + toNotify: cmd.data.toNotify, + isActive: cmd.data.isActive, + createdViaUI: true, + })); + if (value) { + console.log(`Search value: "${value}"`); + const updatedView = await listWorkflowContextualBar( + transformedChatWorkflows, + transformedUIWorkflows, + value as string, + ); + + return context.getInteractionResponder().updateContextualBarViewResponse(updatedView); + } else { + const updatedView = await listWorkflowContextualBar( + transformedChatWorkflows, + transformedUIWorkflows, + ); + + return context.getInteractionResponder().updateContextualBarViewResponse(updatedView); + } + } + + // Handle close button + if (actionId === WorkflowContextualBarEnum.CLOSE_ACTION_ID) { + return context.getInteractionResponder().successResponse(); + } + + return context.getInteractionResponder().successResponse(); + } +} diff --git a/handler/ExecuteViewSubmitHandler.ts b/handler/ExecuteViewSubmitHandler.ts index 4095144..83fd87e 100644 --- a/handler/ExecuteViewSubmitHandler.ts +++ b/handler/ExecuteViewSubmitHandler.ts @@ -7,9 +7,16 @@ import { import { UIKitViewSubmitInteractionContext } from "@rocket.chat/apps-engine/definition/uikit"; import { AiChatWorkflowsAutomationApp } from "../AiChatWorkflowsAutomationApp"; import { Modals } from "../definitions/ModalsEnum"; -import { getRoom, saveTriggerResponse } from "../utils/PersistenceMethods"; +import { + getRoom, + saveTriggerResponse, + updateTriggerResponse, + deleteTriggerResponse, +} from "../utils/PersistenceMethods"; import { sendNotification } from "../utils/Messages"; import { MessageEnum } from "../definitions/MessageEnum"; +import { DeleteWorkflowModalEnum } from "../definitions/ModalsEnum/DeleteWorkflowModal"; +import { EditWorkflowModalEnum } from "../definitions/ModalsEnum/Editworkflow"; export class ExecuteViewSubmitHandler { constructor( @@ -18,29 +25,28 @@ export class ExecuteViewSubmitHandler { private readonly http: IHttp, private readonly modify: IModify, private readonly persistence: IPersistence - ) {} + ) { } public async run(context: UIKitViewSubmitInteractionContext) { const { user, view } = context.getInteractionData(); - if (!user) { - return { - success: false, - error: "No user found", - }; + return { success: false, error: "No user found" }; } - const modalId = view.id; + console.log(`[ExecuteViewSubmitHandler] Modal ID: ${modalId}`); + if (modalId.startsWith(EditWorkflowModalEnum.VIEW_ID)) { + return await this.handleEditWorkflowModal(context); + } switch (modalId) { case Modals.AutomationCreate: return await this.handleAutomationCreateModal(context); + case DeleteWorkflowModalEnum.VIEW_ID: + return await this.handleDeleteWorkflowModal(context); + default: - return { - success: false, - error: "Unknown modal ID", - }; + return { success: false, error: "Unknown modal ID" }; } } @@ -48,7 +54,6 @@ export class ExecuteViewSubmitHandler { context: UIKitViewSubmitInteractionContext ) { const { user, view } = context.getInteractionData(); - const action = view.state?.["actionBlock"]?.["action"] || ""; const users = view.state?.["usersBlock"]?.["users"] || ""; const channels = view.state?.["channelsBlock"]?.["channels"] || ""; @@ -62,19 +67,12 @@ export class ExecuteViewSubmitHandler { if (users && channels && condition && action && response) { const command = `When the user @${users} sends a message in the #${channels} channel that includes the phrase "${condition}", then perform the action "${action}" with response '${response}'.`; - const id = await saveTriggerResponse( + await saveTriggerResponse( this.persistence, { command: command, - trigger: { - user: users, - channel: channels, - condition: condition, - }, - response: { - action: action, - message: response, - }, + trigger: { user: users, channel: channels, condition: condition }, + response: { action: action, message: response }, }, user.id, false, @@ -82,7 +80,7 @@ export class ExecuteViewSubmitHandler { true ); - const { room, error } = await getRoom(this.read, user.id); + const { room } = await getRoom(this.read, user.id); if (room) { await sendNotification( this.read, @@ -93,15 +91,98 @@ export class ExecuteViewSubmitHandler { ); } - return { - success: true, - ...view, - }; - } else { - return { - success: false, - ...view, - }; + return { success: true, ...view }; + } + + return { success: false, ...view }; + } + + private async handleEditWorkflowModal( + context: UIKitViewSubmitInteractionContext + ) { + const { user, view } = context.getInteractionData(); + + // Extract workflow ID from view.id (format: "edit_workflow_modal---workflowId_123") + const workflowId = view.id.split('---')[1]; + + if (!workflowId) { + return { success: false, error: "Workflow ID not found", ...view }; + } + const users = view.state?.[EditWorkflowModalEnum.USERS_BLOCK]?.[EditWorkflowModalEnum.USERS_ACTION] || ""; + const channels = view.state?.[EditWorkflowModalEnum.CHANNELS_BLOCK]?.[EditWorkflowModalEnum.CHANNELS_ACTION] || ""; + const condition = view.state?.[EditWorkflowModalEnum.CONDITION_BLOCK]?.[EditWorkflowModalEnum.CONDITION_ACTION] || ""; + const action = view.state?.[EditWorkflowModalEnum.ACTION_BLOCK]?.[EditWorkflowModalEnum.ACTION_ACTION] || ""; + let response = view.state?.[EditWorkflowModalEnum.RESPONSE_BLOCK]?.[EditWorkflowModalEnum.RESPONSE_ACTION] || ""; + const notify = view.state?.[EditWorkflowModalEnum.NOTIFY_BLOCK]?.[EditWorkflowModalEnum.NOTIFY_ACTION] === "true"; + const active = view.state?.[EditWorkflowModalEnum.ACTIVE_BLOCK]?.[EditWorkflowModalEnum.ACTIVE_ACTION] === "true"; + + if (action === "delete-message") { + response = "N/A"; + } + + if (users && channels && condition && action) { + try { + const command = `When the user @${users} sends a message in the #${channels} channel that includes the phrase "${condition}", then perform the action "${action}" with response '${response}'.`; + + await updateTriggerResponse( + this.persistence, + this.read, + workflowId, + { + command, + trigger: { user: users, channel: channels, condition: condition }, + response: { action, message: response }, + toNotify: notify, + isActive: active, + } + ); + const { room } = await getRoom(this.read, user.id); + if (room) { + await sendNotification( + this.read, + this.modify, + user, + room, + `βœ… Workflow updated successfully!\n${command}` + ); + } + return { success: true, ...view }; + } catch (error) { + console.error('[handleEditWorkflowModal] Error:', error); + return { success: false, error: "Failed to update workflow", ...view }; + } + } + return { success: false, error: "Missing required fields", ...view }; + } + + private async handleDeleteWorkflowModal( + context: UIKitViewSubmitInteractionContext + ) { + const { user, view } = context.getInteractionData(); + + const workflowId = view.state?.[DeleteWorkflowModalEnum.WORKFLOW_ID_BLOCK]?.["workflow_id_input"] || ""; + + if (workflowId) { + try { + await deleteTriggerResponse(this.persistence, workflowId); + + const { room } = await getRoom(this.read, user.id); + if (room) { + await sendNotification( + this.read, + this.modify, + user, + room, + `πŸ—‘οΈ Workflow deleted successfully (ID: ${workflowId})` + ); + } + return { success: true, ...view }; + } catch (error) { + console.error('[handleDeleteWorkflowModal] Error:', error); + return { success: false, error: "Failed to delete workflow", ...view }; + } } + + return { success: false, error: "No workflow ID provided", ...view }; } } diff --git a/modals/DeleteModal.ts b/modals/DeleteModal.ts new file mode 100644 index 0000000..e7cd833 --- /dev/null +++ b/modals/DeleteModal.ts @@ -0,0 +1,81 @@ +import { TextObjectType } from '@rocket.chat/ui-kit'; +import { UIKitSurfaceType } from '@rocket.chat/apps-engine/definition/uikit'; +import { DeleteWorkflowModalEnum } from '../definitions/ModalsEnum/DeleteWorkflowModal'; + + +export async function createDeleteWorkflowModal( + workflowId: string, + workflowCommand?: string +): Promise { + const blocks: any[] = []; + + blocks.push({ + type: 'section', + text: { + type: TextObjectType.MRKDWN, + text: `⚠️ *Are you sure you want to delete this workflow?*`, + }, + }); + + blocks.push({ + type: 'section', + text: { + type: TextObjectType.MRKDWN, + text: `*Workflow ID:* ${workflowId}${workflowCommand ? `\n*Command:* ${workflowCommand.slice(0, 100)}` : ''}`, + }, + }); + + blocks.push({ + type: 'divider', + }); + + blocks.push({ + type: 'section', + text: { + type: TextObjectType.PLAIN_TEXT, + text: 'This action cannot be undone. The workflow will be permanently deleted.', + }, + }); + + blocks.push({ + type: 'input', + blockId: DeleteWorkflowModalEnum.WORKFLOW_ID_BLOCK, + label: { + type: TextObjectType.PLAIN_TEXT, + text: 'Workflow ID', + }, + element: { + type: 'plain_text_input', + actionId: 'workflow_id_input', + initialValue: workflowId, + }, + optional: false, + }); + + return { + id: DeleteWorkflowModalEnum.VIEW_ID, + type: UIKitSurfaceType.MODAL, + title: { + type: TextObjectType.PLAIN_TEXT, + text: 'Delete Workflow', + }, + blocks, + close: { + type: 'button', + text: { + type: TextObjectType.PLAIN_TEXT, + text: 'Cancel', + }, + actionId: DeleteWorkflowModalEnum.CANCEL_ACTION, + }, + submit: { + type: 'button', + text: { + type: TextObjectType.PLAIN_TEXT, + text: 'Delete', + }, + actionId: DeleteWorkflowModalEnum.SUBMIT_ACTION, + style: 'danger', + }, + }; +} diff --git a/modals/EditModal.ts b/modals/EditModal.ts new file mode 100644 index 0000000..cbbbaf9 --- /dev/null +++ b/modals/EditModal.ts @@ -0,0 +1,234 @@ +import { BlockElementType, TextObjectType } from '@rocket.chat/ui-kit'; +import { UIKitSurfaceType, } from '@rocket.chat/apps-engine/definition/uikit'; +import { TriggerResponseData } from '../utils/PersistenceMethods'; +import { EditWorkflowModalEnum } from '../definitions/ModalsEnum/Editworkflow'; + + +export async function createEditWorkflowModal( + workflow: TriggerResponseData +): Promise { + const blocks: any[] = []; + + const actionOptions = [ + { text: 'Send Message', value: 'send-message' }, + { text: 'Reply to Message', value: 'reply-message' }, + { text: 'Delete Message', value: 'delete-message' }, + ]; + + const notifyOptions = [ + { text: 'Yes', value: 'true' }, + { text: 'No', value: 'false' }, + ]; + const activeOptions = [ + { text: 'Enabled', value: 'true' }, + { text: 'Disabled', value: 'false' }, + ]; + + blocks.push({ + type: 'section', + text: { + type: TextObjectType.MRKDWN, + text: `*Edit Workflow*\n_ID: \`${workflow.id}\`_`, + }, + }); + + blocks.push({ type: 'divider' }); + + blocks.push({ + type: 'input', + blockId: EditWorkflowModalEnum.USERS_BLOCK, + label: { + type: TextObjectType.PLAIN_TEXT, + text: 'User', + }, + element: { + type: 'plain_text_input', + actionId: EditWorkflowModalEnum.USERS_ACTION, + placeholder: { + type: TextObjectType.PLAIN_TEXT, + text: 'Enter username', + }, + initialValue: workflow.trigger?.user || '', + }, + }); + + blocks.push({ + type: 'input', + blockId: EditWorkflowModalEnum.CHANNELS_BLOCK, + label: { + type: TextObjectType.PLAIN_TEXT, + text: 'Channel', + }, + element: { + type: 'plain_text_input', + actionId: EditWorkflowModalEnum.CHANNELS_ACTION, + placeholder: { + type: TextObjectType.PLAIN_TEXT, + text: 'Enter channel name', + }, + initialValue: workflow.trigger?.channel || '', + }, + }); + + blocks.push({ + type: 'input', + blockId: EditWorkflowModalEnum.CONDITION_BLOCK, + label: { + type: TextObjectType.PLAIN_TEXT, + text: 'Trigger Condition', + }, + element: { + type: 'plain_text_input', + actionId: EditWorkflowModalEnum.CONDITION_ACTION, + placeholder: { + type: TextObjectType.PLAIN_TEXT, + text: 'Enter trigger phrase', + }, + initialValue: workflow.trigger?.condition || '', + }, + }); + + const currentAction = workflow.response?.action || 'send-message'; + const selectedAction = actionOptions.find(opt => opt.value === currentAction); + + blocks.push({ + type: 'input', + blockId: EditWorkflowModalEnum.ACTION_BLOCK, + label: { + type: TextObjectType.PLAIN_TEXT, + text: 'Action', + }, + element: { + type: BlockElementType.STATIC_SELECT, + actionId: EditWorkflowModalEnum.ACTION_ACTION, + placeholder: { + type: TextObjectType.PLAIN_TEXT, + text: 'Select action', + }, + initialOption: { + text: { + type: TextObjectType.PLAIN_TEXT, + text: selectedAction?.text || 'Send Message', + }, + value: selectedAction?.value || 'send-message', + }, + options: actionOptions.map(opt => ({ + text: { + type: TextObjectType.PLAIN_TEXT, + text: opt.text, + }, + value: opt.value, + })), + }, + }); + + blocks.push({ + type: 'input', + blockId: EditWorkflowModalEnum.RESPONSE_BLOCK, + label: { + type: TextObjectType.PLAIN_TEXT, + text: 'Response Message', + }, + element: { + type: 'plain_text_input', + actionId: EditWorkflowModalEnum.RESPONSE_ACTION, + multiline: true, + placeholder: { + type: TextObjectType.PLAIN_TEXT, + text: 'Enter response message', + }, + initialValue: workflow.response?.message || '', + }, + optional: true, + }); + + const selectedNotify = workflow.toNotify ? notifyOptions[0] : notifyOptions[1]; + + blocks.push({ + type: 'input', + blockId: EditWorkflowModalEnum.NOTIFY_BLOCK, + label: { + type: TextObjectType.PLAIN_TEXT, + text: 'Enable Notifications', + }, + element: { + type: BlockElementType.STATIC_SELECT, + actionId: EditWorkflowModalEnum.NOTIFY_ACTION, + placeholder: { + type: TextObjectType.PLAIN_TEXT, + text: 'Select preference', + }, + initialOption: { + text: { + type: TextObjectType.PLAIN_TEXT, + text: selectedNotify.text, + }, + value: selectedNotify.value, + }, + options: notifyOptions.map(opt => ({ + text: { + type: TextObjectType.PLAIN_TEXT, + text: opt.text, + }, + value: opt.value, + })), + }, + }); + + const selectedActive = workflow.isActive ? activeOptions[0] : activeOptions[1]; + + blocks.push({ + type: 'input', + blockId: EditWorkflowModalEnum.ACTIVE_BLOCK, + label: { + type: TextObjectType.PLAIN_TEXT, + text: 'Workflow Status', + }, + element: { + type: BlockElementType.STATIC_SELECT, + actionId: EditWorkflowModalEnum.ACTIVE_ACTION, + placeholder: { + type: TextObjectType.PLAIN_TEXT, + text: 'Select status', + }, + initialOption: { + text: { + type: TextObjectType.PLAIN_TEXT, + text: selectedActive.text, + }, + value: selectedActive.value, + }, + options: activeOptions.map(opt => ({ + text: { + type: TextObjectType.PLAIN_TEXT, + text: opt.text, + }, + value: opt.value, + })), + }, + }); + + return { + id: `${EditWorkflowModalEnum.VIEW_ID}---${workflow.id}`, + type: UIKitSurfaceType.MODAL, + title: { + type: TextObjectType.PLAIN_TEXT, + text: 'Edit Workflow', + }, + blocks, + close: { + type: 'button', + text: { + type: TextObjectType.PLAIN_TEXT, + text: 'Cancel', + }, + }, + submit: { + type: 'button', + text: { + type: TextObjectType.PLAIN_TEXT, + text: 'Save Changes', + }, + }, + }; +} diff --git a/modals/ListContextualBar.ts b/modals/ListContextualBar.ts new file mode 100644 index 0000000..c60bc1c --- /dev/null +++ b/modals/ListContextualBar.ts @@ -0,0 +1,213 @@ +import { + IUIKitSurfaceViewParam, +} from '@rocket.chat/apps-engine/definition/accessors'; +import { TextObjectType } from '@rocket.chat/ui-kit'; +import { UIKitSurfaceType } from '@rocket.chat/apps-engine/definition/uikit'; +import { BlockElementType } from '@rocket.chat/apps-engine/definition/uikit'; +import { IWorkflow, ModalsAction, WorkflowContextualBarEnum } from '../definitions/ModalsEnum/ListContextualBar'; + + +export async function listWorkflowContextualBar( + chatWorkflows: IWorkflow[], + uiWorkflows: IWorkflow[], + searchValue?: string, +): Promise { + const blocks: any[] = []; + + // Filter workflows based on search value + const searchValueLowerCase = searchValue?.toLowerCase(); + + let filteredChatWorkflows = chatWorkflows; + let filteredUIWorkflows = uiWorkflows; + + if (searchValueLowerCase) { + filteredChatWorkflows = chatWorkflows.filter((workflow) => { + return ( + workflow.id.toLowerCase().includes(searchValueLowerCase) || + workflow.command.toLowerCase().includes(searchValueLowerCase) + ); + }); + + filteredUIWorkflows = uiWorkflows.filter((workflow) => { + return ( + workflow.id.toLowerCase().includes(searchValueLowerCase) || + workflow.command.toLowerCase().includes(searchValueLowerCase) + ); + }); + } + + const sortedChatWorkflows = filteredChatWorkflows.sort((a, b) => { + return a.command.localeCompare(b.command); + }); + + const sortedUIWorkflows = filteredUIWorkflows.sort((a, b) => { + return a.command.localeCompare(b.command); + }); + + // Search Input Block + blocks.push({ + type: 'input', + blockId: WorkflowContextualBarEnum.SEARCH_BLOCK_ID, + label: { + text: 'Search Workflows', + type: TextObjectType.PLAINTEXT, + }, + element: { + type: 'plain_text_input', + actionId: WorkflowContextualBarEnum.SEARCH_ACTION_ID, + placeholder: { + text: 'Search by ID or command...', + type: TextObjectType.PLAINTEXT, + }, + initialValue: searchValue || '', + dispatchActionConfig: [ModalsAction.dispatchActionConfigOnInput], + }, + }); + + blocks.push({ + type: 'divider', + }); + + if (sortedChatWorkflows.length > 0) { + blocks.push({ + type: 'section', + text: { + type: TextObjectType.MRKDWN, + text: '*Workflows Created via Chat*', + }, + }); + + blocks.push({ + type: 'divider', + }); + + sortedChatWorkflows.forEach((workflow) => { + // Workflow Command Section with Overflow Menu + blocks.push({ + type: 'section', + text: { + type: TextObjectType.MRKDWN, + text: `*Command:* ${workflow.command.slice(0, 40)}`, + }, + accessory: { + type: BlockElementType.OVERFLOW_MENU, + actionId: `${WorkflowContextualBarEnum.OVERFLOW_ACTION_ID}_${workflow.id}`, + options: [ + { + text: { + type: TextObjectType.PLAIN_TEXT, + text: 'Edit', + }, + value: `${WorkflowContextualBarEnum.EDIT_ACTION_VALUE}_${workflow.id}`, + }, + { + text: { + type: TextObjectType.PLAIN_TEXT, + text: 'Delete', + }, + value: `${WorkflowContextualBarEnum.DELETE_ACTION_VALUE}_${workflow.id}`, + }, + ], + }, + }); + + // Workflow Details Context + blocks.push({ + type: 'context', + elements: [ + { + type: TextObjectType.MRKDWN, + text: `*ID:* ${workflow.id} | *Notification:* ${workflow.toNotify ? 'βœ… ON' : '❌ OFF'} | *Status:* ${workflow.isActive ? '🟒 Enabled' : 'πŸ”΄ Disabled'}`, + }, + ], + }); + + blocks.push({ + type: 'divider', + }); + }); + } + + // UI-based Workflows Section + if (sortedUIWorkflows.length > 0) { + blocks.push({ + type: 'section', + text: { + type: TextObjectType.MRKDWN, + text: '*Workflows Created via UI*', + }, + }); + + blocks.push({ + type: 'divider', + }); + + sortedUIWorkflows.forEach((workflow) => { + // Workflow Command Section with Overflow Menu + blocks.push({ + type: 'section', + text: { + type: TextObjectType.MRKDWN, + text: `*Command:* ${workflow.command.slice(0, 40)}`, + }, + accessory: { + type: BlockElementType.OVERFLOW_MENU, + actionId: `${WorkflowContextualBarEnum.OVERFLOW_ACTION_ID}_${workflow.id}`, + options: [ + { + text: { + type: TextObjectType.PLAIN_TEXT, + text: 'Edit', + }, + value: `${WorkflowContextualBarEnum.EDIT_ACTION_VALUE}_${workflow.id}`, + }, + { + text: { + type: TextObjectType.PLAIN_TEXT, + text: 'Delete', + }, + value: `${WorkflowContextualBarEnum.DELETE_ACTION_VALUE}_${workflow.id}`, + }, + ], + }, + }); + + // Workflow Details Context + blocks.push({ + type: 'context', + elements: [ + { + type: TextObjectType.MRKDWN, + text: `*ID:* ${workflow.id} | *Notification:* ${workflow.toNotify ? 'βœ… ON' : '❌ OFF'} | *Status:* ${workflow.isActive ? '🟒 Enabled' : 'πŸ”΄ Disabled'}`, + }, + ], + }); + + blocks.push({ + type: 'divider', + }); + }); + } + + if (sortedChatWorkflows.length === 0 && sortedUIWorkflows.length === 0) { + blocks.push({ + type: 'section', + text: { + type: TextObjectType.PLAIN_TEXT, + text: searchValue + ? 'No workflows found matching your search.' + : 'No workflows found. Create your first workflow to get started!', + }, + }); + } + + return { + id: WorkflowContextualBarEnum.VIEW_ID, + type: UIKitSurfaceType.CONTEXTUAL_BAR, + title: { + type: TextObjectType.PLAIN_TEXT, + text: 'Workflows List', + }, + blocks, + }; +} diff --git a/slashCommands/ChatAutomation.ts b/slashCommands/ChatAutomation.ts index 7b93eaf..f2c1a93 100644 --- a/slashCommands/ChatAutomation.ts +++ b/slashCommands/ChatAutomation.ts @@ -10,23 +10,15 @@ import { } from "@rocket.chat/apps-engine/definition/slashcommands"; import { AiChatWorkflowsAutomationApp } from "../AiChatWorkflowsAutomationApp"; import { - deleteTriggerResponse, findTriggerResponsesByCreatorAndLLM, - updateIsActiveStatus, - updateToNotifyStatus, } from "../utils/PersistenceMethods"; import { IUser } from "@rocket.chat/apps-engine/definition/users"; import { MessageEnum } from "../definitions/MessageEnum"; -import { - sendDirectMessage, - sendNotification, - sendMessageInChannel, - sendThreadMessage, -} from "../utils/Messages"; -import { IMessageRaw } from "@rocket.chat/apps-engine/definition/messages"; +import { sendDirectMessage, sendNotification } from "../utils/Messages"; +import { listWorkflowContextualBar } from "../modals/ListContextualBar"; export class ChatAutomation implements ISlashCommand { - public constructor(private readonly app: AiChatWorkflowsAutomationApp) {} + public constructor(private readonly app: AiChatWorkflowsAutomationApp) { } public command = "chat-automation"; public i18nDescription = "chat automation config"; @@ -46,219 +38,23 @@ export class ChatAutomation implements ISlashCommand { const appUser = (await read.getUserReader().getAppUser()) as IUser; - const command = context.getArguments(); const [subcommand] = context.getArguments(); const filter = subcommand ? subcommand.toLowerCase() : ""; if (filter === "list") { - const userCommands = await findTriggerResponsesByCreatorAndLLM( - read, - user.id, - true - ); - - let messageToSend: string; - - if (userCommands.length > 0) { - let counter = 1; - messageToSend = userCommands - .map((command) => { - const line = `${counter}. *Id*: ${command.data.id} - *Command*: ${command.data.command} - *Notification*: ${command.data.toNotify ? "ON" : "OFF"} - *Active Status*: ${ - command.data.isActive ? "Enabled" : "Disabled" - }\n`; - counter++; - return line; - }) - .join("\n"); - } else { - messageToSend = MessageEnum.WORKFLOW_NOT_FOUND_DM; - } - - await sendNotification( - read, - modify, - user, - room, - `Created using chat: - ${messageToSend}`, - threadId - ); - - /* - const messages: IMessageRaw[] = await read - .getRoomReader() - .getMessages(room.id, { - limit: Math.min(1), - sort: { createdAt: "desc" }, - }); - - const newThreadId = messages[0]?.id; - if (newThreadId) { - await sendThreadMessage( - read, - modify, - appUser, - room, - messageToSend, - newThreadId - ); - } - */ - - // ================================================================== - const userCommandsUI = await findTriggerResponsesByCreatorAndLLM( - read, - user.id, - false - ); - - let messageToSendUI: string; - - if (userCommandsUI.length > 0) { - let counter = 1; - messageToSendUI = userCommandsUI - .map((command) => { - const line = `${counter}. *Id*: ${command.data.id} - *Command*: ${command.data.command} - *Notification*: ${command.data.toNotify ? "ON" : "OFF"} - *Active Status*: ${ - command.data.isActive ? "Enabled" : "Disabled" - }\n`; - counter++; - return line; - }) - .join("\n"); - } else { - messageToSendUI = MessageEnum.WORKFLOW_NOT_FOUND_UI; - } - - await sendNotification( - read, - modify, - user, - room, - `Created using UI Block: - ${messageToSendUI}`, - threadId - ); - - /* - const messagesForUI: IMessageRaw[] = await read - .getRoomReader() - .getMessages(room.id, { - limit: Math.min(1), - sort: { createdAt: "desc" }, - }); - - const newThreadIdUI = messagesForUI[0]?.id; - if (newThreadIdUI) { - await sendThreadMessage( - read, - modify, - appUser, - room, - messageToSendUI, - newThreadIdUI - ); - } - */ - } else if (filter === "delete") { - if (command[1]) { - await deleteTriggerResponse(persistence, command[1]); - - await sendNotification( - read, - modify, - user, - room, - `Deleted the workflow with id ${command[1]}`, - threadId - ); - } - } else if (filter === "notification") { - if (command[1]) { - if (command[1].toLocaleLowerCase() === "off") { - if (command[2]) { - await updateToNotifyStatus( - persistence, - read, - command[2], - false - ); - - await sendNotification( - read, - modify, - user, - room, - `Notification config updated to 'OFF' for workflow with id: ${command[2]}`, - threadId - ); - } - } else if (command[1].toLocaleLowerCase() === "on") { - if (command[2]) { - await updateToNotifyStatus( - persistence, - read, - command[2], - true - ); - - await sendNotification( - read, - modify, - user, - room, - `Notification config updated to 'ON' for workflow with id: ${command[2]}`, - threadId - ); - } - } - } - } else if (filter === "enable") { - if (command[1]) { - await updateIsActiveStatus(persistence, read, command[1], true); - - await sendNotification( - read, - modify, - user, - room, - `Automation workflow with id: ${command[1]} is now enabled.`, - threadId - ); - } - } else if (filter === "disable") { - if (command[1]) { - await updateIsActiveStatus( - persistence, - read, - command[1], - false - ); - await sendNotification( - read, - modify, - user, - room, - `Automation workflow with id: ${command[1]} is now disabled.`, - threadId - ); - } + await this.openWorkflowListContextualBar(context, read, modify); } else if (filter === "ping") { await sendDirectMessage( read, modify, user, `_Hello ${user.name}, I'm ${appUser.name} App. I can help you create Chat Automation workflows!_ - Here’s how it works: + Here's how it works: _"Whenever @ posts any welcome messages in #, immediately DM them with a thank-you note."_ _Just describe what you'd like to automate, and I'll take care of the rest!_` ); } else { + // Show help message for unknown commands await sendNotification( read, modify, @@ -269,4 +65,58 @@ export class ChatAutomation implements ISlashCommand { ); } } + + private async openWorkflowListContextualBar( + context: SlashCommandContext, + read: IRead, + modify: IModify, + ): Promise { + const room = context.getRoom(); + const user = context.getSender(); + + // Fetch chat-based workflows (usedLLM = true) + const chatWorkflows = await findTriggerResponsesByCreatorAndLLM( + read, + user.id, + true + ); + + // Fetch UI-based workflows (usedLLM = false) + const uiWorkflows = await findTriggerResponsesByCreatorAndLLM( + read, + user.id, + false + ); + + // Transform the data to match the interface expected by the contextual bar + const transformedChatWorkflows = chatWorkflows.map((cmd) => ({ + id: cmd.data.id, + command: cmd.data.command, + toNotify: cmd.data.toNotify, + isActive: cmd.data.isActive, + createdViaUI: false, + })); + + const transformedUIWorkflows = uiWorkflows.map((cmd) => ({ + id: cmd.data.id, + command: cmd.data.command, + toNotify: cmd.data.toNotify, + isActive: cmd.data.isActive, + createdViaUI: true, + })); + + // Create the contextual bar view + const contextualBarView = await listWorkflowContextualBar( + transformedChatWorkflows, + transformedUIWorkflows, + ); + + // Open the contextual bar + const triggerId = context.getTriggerId(); + if (triggerId) { + await modify + .getUiController() + .openSurfaceView(contextualBarView, { triggerId }, user); + } + } }