From c7c828b56e7a5f877ef0a78498018ed930bccb23 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Wed, 25 Feb 2026 18:28:06 +0100 Subject: [PATCH] feat: add resume and cancel button text options to Slack approval API + formatted args + typo (#8095) --- backend/windmill-api/openapi.yaml | 20 +++++++++++ backend/windmill-api/src/approvals.rs | 15 +++++++-- backend/windmill-api/src/slack_approvals.rs | 37 ++++++++++++++++++--- typescript-client/client.ts | 15 +++++++++ 4 files changed, 80 insertions(+), 7 deletions(-) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index c958a4c03c..c18a1fbf11 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -10680,6 +10680,16 @@ paths: required: false schema: type: string + - name: resume_button_text + in: query + required: false + schema: + type: string + - name: cancel_button_text + in: query + required: false + schema: + type: string responses: "200": description: Interactive slack approval message sent successfully @@ -10726,6 +10736,16 @@ paths: required: false schema: type: string + - name: resume_button_text + in: query + required: false + schema: + type: string + - name: cancel_button_text + in: query + required: false + schema: + type: string responses: "200": description: Interactive slack approval message sent successfully diff --git a/backend/windmill-api/src/approvals.rs b/backend/windmill-api/src/approvals.rs index e2344adc4e..fe9adb602e 100644 --- a/backend/windmill-api/src/approvals.rs +++ b/backend/windmill-api/src/approvals.rs @@ -83,6 +83,12 @@ pub struct QueryDynamicEnumJson { pub dynamic_enums_json: Option, } +#[derive(Deserialize, Debug)] +pub struct QueryButtonText { + pub resume_button_text: Option, + pub cancel_button_text: Option, +} + #[derive(Debug)] pub struct ApprovalFormDetails { pub message_str: String, @@ -266,7 +272,12 @@ pub async fn get_approval_form_details( }) }); - let args_str = args.map_or("None".to_string(), |a| a.get().to_string()); + let args_str = args.map_or("None".to_string(), |a| { + serde_json::from_str::(a.get()) + .ok() + .and_then(|v| serde_json::to_string_pretty(&v).ok()) + .unwrap_or_else(|| a.get().to_string()) + }); let parent_job_id_str = parent_job_id.map_or("None".to_string(), |id| id.to_string()); let script_path_str = script_path.as_deref().unwrap_or("None"); @@ -282,7 +293,7 @@ pub async fn get_approval_form_details( {}: {created_by}\n\n\ {}: {created_at_formatted}\n\n\ {}: {script_path_str}\n\n\ - {}: {args_str}\n\n\ + {}:\n```\n{args_str}\n```\n\n\ {}: {parent_job_id_str}\n\n", bold_format.replace("{}", "Created by"), bold_format.replace("{}", "Created at"), diff --git a/backend/windmill-api/src/slack_approvals.rs b/backend/windmill-api/src/slack_approvals.rs index ed6368dada..34a79b4651 100644 --- a/backend/windmill-api/src/slack_approvals.rs +++ b/backend/windmill-api/src/slack_approvals.rs @@ -16,8 +16,8 @@ use crate::jobs::{QueryApprover, ResumeUrls}; use crate::{ approvals::{ extract_w_id_from_resume_url, handle_resume_action, ApprovalFormDetails, FieldType, - MessageFormat, QueryDefaultArgsJson, QueryDynamicEnumJson, QueryFlowStepId, QueryMessage, - ResumeFormField, ResumeSchema, + MessageFormat, QueryButtonText, QueryDefaultArgsJson, QueryDynamicEnumJson, + QueryFlowStepId, QueryMessage, ResumeFormField, ResumeSchema, }, auth::OptTokened, }; @@ -107,6 +107,8 @@ struct ModalActionValue { flow_step_id: Option, default_args_json: Option, dynamic_enums_json: Option, + resume_button_text: Option, + cancel_button_text: Option, } #[derive(Deserialize, Debug)] @@ -200,6 +202,8 @@ pub async fn slack_app_callback_handler( container, default_args_json.as_ref(), dynamic_enums_json.as_ref(), + parsed_value.resume_button_text.as_deref(), + parsed_value.cancel_button_text.as_deref(), ) .await .map_err(|e| Error::BadRequest(e.to_string()))?; @@ -229,6 +233,7 @@ pub async fn request_slack_approval( Query(flow_step_id): Query, Query(default_args_json): Query, Query(dynamic_enums_json): Query, + Query(button_text): Query, ) -> Result { let slack_resource_path = slack_resource_path.slack_resource_path; let channel_id = channel_id.channel_id; @@ -255,6 +260,8 @@ pub async fn request_slack_approval( flow_step_id.as_str(), default_args_json.default_args_json.as_ref(), dynamic_enums_json.dynamic_enums_json.as_ref(), + button_text.resume_button_text.as_deref(), + button_text.cancel_button_text.as_deref(), ) .await .map_err(|e| Error::BadRequest(e.to_string()))?; @@ -752,6 +759,8 @@ async fn send_slack_message( flow_step_id: &str, default_args_json: Option<&serde_json::Value>, dynamic_enums_json: Option<&serde_json::Value>, + resume_button_text: Option<&str>, + cancel_button_text: Option<&str>, ) -> Result> { let url = "https://slack.com/api/chat.postMessage"; @@ -779,6 +788,14 @@ async fn send_slack_message( value["dynamic_enums_json"] = dynamic_enums_json.clone(); } + if let Some(resume_button_text) = resume_button_text { + value["resume_button_text"] = serde_json::json!(resume_button_text); + } + + if let Some(cancel_button_text) = cancel_button_text { + value["cancel_button_text"] = serde_json::json!(cancel_button_text); + } + let payload = serde_json::json!({ "channel": channel_id, "text": "A flow has been suspended. Please approve or reject the flow.", @@ -842,6 +859,8 @@ async fn get_modal_blocks( container: Container, default_args_json: Option<&serde_json::Value>, dynamic_enums_json: Option<&serde_json::Value>, + resume_button_text: Option<&str>, + cancel_button_text: Option<&str>, ) -> Result, Error> { let approval_details = crate::approvals::get_approval_form_details( db, @@ -895,6 +914,8 @@ async fn get_modal_blocks( &urls.resume, resource_path, container, + resume_button_text, + cancel_button_text, ))) } @@ -905,6 +926,8 @@ fn construct_payload( resume_url: &str, resource_path: &str, container: Container, + resume_button_text: Option<&str>, + cancel_button_text: Option<&str>, ) -> serde_json::Value { let mut view = serde_json::json!({ "type": "modal", @@ -912,12 +935,12 @@ fn construct_payload( "notify_on_close": true, "title": { "type": "plain_text", - "text": "Worfklow Suspended" + "text": "Workflow Suspended" }, "blocks": blocks, "submit": { "type": "plain_text", - "text": "Resume Workflow" + "text": resume_button_text.unwrap_or("Resume Workflow") }, "private_metadata": serde_json::json!({ "resume_url": resume_url, "resource_path": resource_path, "container": container, "hide_cancel": hide_cancel }).to_string(), }); @@ -925,7 +948,7 @@ fn construct_payload( if !hide_cancel { view["close"] = serde_json::json!({ "type": "plain_text", - "text": "Cancel Workflow" + "text": cancel_button_text.unwrap_or("Cancel Workflow") }); } @@ -949,6 +972,8 @@ async fn open_modal_with_blocks( container: Container, default_args_json: Option<&serde_json::Value>, dynamic_enums_json: Option<&serde_json::Value>, + resume_button_text: Option<&str>, + cancel_button_text: Option<&str>, ) -> Result<(), Box> { let resume_id = rand::random::(); let blocks_json = match get_modal_blocks( @@ -964,6 +989,8 @@ async fn open_modal_with_blocks( container, default_args_json, dynamic_enums_json, + resume_button_text, + cancel_button_text, ) .await { diff --git a/typescript-client/client.ts b/typescript-client/client.ts index 60caf10a46..96abcdd4be 100644 --- a/typescript-client/client.ts +++ b/typescript-client/client.ts @@ -1169,6 +1169,8 @@ interface SlackApprovalOptions { approver?: string; defaultArgsJson?: Record; dynamicEnumsJson?: Record; + resumeButtonText?: string; + cancelButtonText?: string; } interface TeamsApprovalOptions { @@ -1208,6 +1210,8 @@ interface TeamsApprovalOptions { * approver: "approver123", * defaultArgsJson: { key1: "value1", key2: 42 }, * dynamicEnumsJson: { foo: ["choice1", "choice2"], bar: ["optionA", "optionB"] }, + * resumeButtonText: "Resume", + * cancelButtonText: "Cancel", * }); * ``` * @@ -1220,6 +1224,8 @@ export async function requestInteractiveSlackApproval({ approver, defaultArgsJson, dynamicEnumsJson, + resumeButtonText, + cancelButtonText, }: SlackApprovalOptions): Promise { const workspace = getWorkspace(); const flowJobId = getEnv("WM_FLOW_JOB_ID"); @@ -1244,6 +1250,8 @@ export async function requestInteractiveSlackApproval({ flowStepId: string; defaultArgsJson?: string; dynamicEnumsJson?: string; + resumeButtonText?: string; + cancelButtonText?: string; } = { slackResourcePath, channelId, @@ -1265,6 +1273,13 @@ export async function requestInteractiveSlackApproval({ params.dynamicEnumsJson = JSON.stringify(dynamicEnumsJson); } + if (resumeButtonText) { + params.resumeButtonText = resumeButtonText; + } + if (cancelButtonText) { + params.cancelButtonText = cancelButtonText; + } + await JobService.getSlackApprovalPayload({ workspace, ...params,