Skip to content

Commit

Permalink
interactive slack approvals: more improvements / nits (#5012)
Browse files Browse the repository at this point in the history
* enum -> enums, client function docs

* advanced -> suspend -> form
  • Loading branch information
alpetric authored Jan 4, 2025
1 parent 0c19171 commit 63b0968
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 38 deletions.
2 changes: 1 addition & 1 deletion backend/windmill-api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6980,7 +6980,7 @@ paths:
required: false
schema:
type: string
- name: dynamic_enum_json
- name: dynamic_enums_json
in: query
required: false
schema:
Expand Down
44 changes: 22 additions & 22 deletions backend/windmill-api/src/slack_approvals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ pub struct QueryDefaultArgsJson {

#[derive(Deserialize, Debug)]
pub struct QueryDynamicEnumJson {
dynamic_enum_json: Option<serde_json::Value>,
dynamic_enums_json: Option<serde_json::Value>,
}

#[derive(Deserialize, Debug)]
Expand All @@ -172,7 +172,7 @@ struct ModalActionValue {
message: Option<String>,
flow_step_id: Option<String>,
default_args_json: Option<String>,
dynamic_enum_json: Option<String>,
dynamic_enums_json: Option<String>,
}

#[derive(Deserialize, Debug)]
Expand Down Expand Up @@ -233,19 +233,19 @@ pub async fn slack_app_callback_handler(
)
})?;

let dynamic_enum_json: Option<serde_json::Value> = parsed_value
.dynamic_enum_json
let dynamic_enums_json: Option<serde_json::Value> = parsed_value
.dynamic_enums_json
.as_deref()
.map(|s| serde_json::from_str(s))
.transpose()
.map_err(|_| {
Error::BadRequest(
"Invalid JSON in dynamic_enum_json".to_string(),
"Invalid JSON in dynamic_enums_json".to_string(),
)
})?;

tracing::debug!("Default args json: {:#?}", default_args_json);
tracing::debug!("Dynamic enum json: {:#?}", dynamic_enum_json);
tracing::debug!("Dynamic enum json: {:#?}", dynamic_enums_json);

open_modal_with_blocks(
&client,
Expand All @@ -260,7 +260,7 @@ pub async fn slack_app_callback_handler(
flow_step_id,
container,
default_args_json.as_ref(),
dynamic_enum_json.as_ref(),
dynamic_enums_json.as_ref(),
)
.await
.map_err(|e| Error::BadRequest(e.to_string()))?;
Expand Down Expand Up @@ -289,7 +289,7 @@ pub async fn request_slack_approval(
Query(channel_id): Query<QueryChannelId>,
Query(flow_step_id): Query<QueryFlowStepId>,
Query(default_args_json): Query<QueryDefaultArgsJson>,
Query(dynamic_enum_json): Query<QueryDynamicEnumJson>,
Query(dynamic_enums_json): Query<QueryDynamicEnumJson>,
) -> Result<StatusCode, Error> {
let slack_resource_path = slack_resource_path.slack_resource_path;
let channel_id = channel_id.channel_id;
Expand All @@ -315,7 +315,7 @@ pub async fn request_slack_approval(
message.message.as_deref(),
flow_step_id.as_str(),
default_args_json.default_args_json.as_ref(),
dynamic_enum_json.dynamic_enum_json.as_ref(),
dynamic_enums_json.dynamic_enums_json.as_ref(),
)
.await
.map_err(|e| Error::BadRequest(e.to_string()))?;
Expand Down Expand Up @@ -430,7 +430,7 @@ async fn transform_schemas(
order: Option<&Vec<String>>,
required: Option<&Vec<String>>,
default_args_json: Option<&serde_json::Value>,
dynamic_enum_json: Option<&serde_json::Value>,
dynamic_enums_json: Option<&serde_json::Value>,
) -> Result<serde_json::Value, Error> {
tracing::debug!("Resume urls: {:#?}", urls);

Expand All @@ -448,10 +448,10 @@ async fn transform_schemas(
let is_required = required.unwrap().contains(key);

let default_value = default_args_json.and_then(|json| json.get(key).cloned());
let dynamic_enum_value = dynamic_enum_json.and_then(|json| json.get(key).cloned());
let dynamic_enums_value = dynamic_enums_json.and_then(|json| json.get(key).cloned());

let input_block =
create_input_block(key, schema, is_required, default_value, dynamic_enum_value);
create_input_block(key, schema, is_required, default_value, dynamic_enums_value);
match input_block {
serde_json::Value::Array(arr) => blocks.extend(arr),
_ => blocks.push(input_block),
Expand All @@ -468,7 +468,7 @@ fn create_input_block(
schema: &ResumeFormField,
required: bool,
default_value: Option<serde_json::Value>,
dynamic_enum_value: Option<serde_json::Value>,
dynamic_enums_value: Option<serde_json::Value>,
) -> serde_json::Value {
let placeholder = schema
.description
Expand Down Expand Up @@ -614,7 +614,7 @@ fn create_input_block(
// Handle enum type
if let Some(enums) = &schema.r#enum {
tracing::debug!("Enum type");
let enums = dynamic_enum_value
let enums = dynamic_enums_value
.as_ref()
.and_then(|v| v.as_array())
.cloned()
Expand Down Expand Up @@ -866,7 +866,7 @@ async fn send_slack_message(
message: Option<&str>,
flow_step_id: &str,
default_args_json: Option<&serde_json::Value>,
dynamic_enum_json: Option<&serde_json::Value>,
dynamic_enums_json: Option<&serde_json::Value>,
) -> Result<StatusCode, Box<dyn std::error::Error>> {
let url = "https://slack.com/api/chat.postMessage";

Expand All @@ -890,8 +890,8 @@ async fn send_slack_message(
value["default_args_json"] = default_args_json.clone();
}

if let Some(dynamic_enum_json) = dynamic_enum_json {
value["dynamic_enum_json"] = dynamic_enum_json.clone();
if let Some(dynamic_enums_json) = dynamic_enums_json {
value["dynamic_enums_json"] = dynamic_enums_json.clone();
}

let payload = serde_json::json!({
Expand Down Expand Up @@ -956,7 +956,7 @@ async fn get_modal_blocks(
resource_path: &str,
container: Container,
default_args_json: Option<&serde_json::Value>,
dynamic_enum_json: Option<&serde_json::Value>,
dynamic_enums_json: Option<&serde_json::Value>,
) -> Result<axum::Json<serde_json::Value>, Error> {
let res = get_resume_urls_internal(
axum::Extension(db.clone()),
Expand Down Expand Up @@ -1065,7 +1065,7 @@ async fn get_modal_blocks(
Some(&inner_schema.schema.order),
Some(&inner_schema.schema.required),
default_args_json,
dynamic_enum_json,
dynamic_enums_json,
)
.await?;

Expand All @@ -1087,7 +1087,7 @@ async fn get_modal_blocks(
None,
None,
default_args_json,
dynamic_enum_json,
dynamic_enums_json,
)
.await?;
return Ok(axum::Json(construct_payload(
Expand Down Expand Up @@ -1156,7 +1156,7 @@ async fn open_modal_with_blocks(
flow_step_id: Option<&str>,
container: Container,
default_args_json: Option<&serde_json::Value>,
dynamic_enum_json: Option<&serde_json::Value>,
dynamic_enums_json: Option<&serde_json::Value>,
) -> Result<(), Box<dyn std::error::Error>> {
let resume_id = rand::random::<u32>();
let blocks_json = match get_modal_blocks(
Expand All @@ -1171,7 +1171,7 @@ async fn open_modal_with_blocks(
resource_path,
container,
default_args_json,
dynamic_enum_json,
dynamic_enums_json,
)
.await
{
Expand Down
51 changes: 41 additions & 10 deletions python-client/wmill/wmill/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,14 +631,45 @@ def request_interactive_slack_approval(
message: str = None,
approver: str = None,
default_args_json: dict = None,
dynamic_enum_json: dict = None,
dynamic_enums_json: dict = None,
) -> None:
"""
Request interactive Slack approval
:param slack_resource_path: Slack resource path
:param channel_id: Slack channel
:param message: Message to send to Slack
:param approver: Approver name
Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields.
**[Enterprise Edition Only]** To include form fields in the Slack approval request, use the "Advanced -> Suspend -> Form" functionality.
Learn more at: https://www.windmill.dev/docs/flows/flow_approval#form
:param slack_resource_path: The path to the Slack resource in Windmill.
:type slack_resource_path: str
:param channel_id: The Slack channel ID where the approval request will be sent.
:type channel_id: str
:param message: Optional custom message to include in the Slack approval request.
:type message: str, optional
:param approver: Optional user ID or name of the approver for the request.
:type approver: str, optional
:param default_args_json: Optional dictionary defining or overriding the default arguments for form fields.
:type default_args_json: dict, optional
:param dynamic_enums_json: Optional dictionary overriding the enum default values of enum form fields.
:type dynamic_enums_json: dict, optional
:raises Exception: If the function is not called within a flow or flow preview.
:raises Exception: If the required flow job or flow step environment variables are not set.
:return: None
**Usage Example:**
>>> client.request_interactive_slack_approval(
... slack_resource_path="/u/alex/my_slack_resource",
... channel_id="admins-slack-channel",
... message="Please approve this request",
... approver="approver123",
... default_args_json={"key1": "value1", "key2": 42},
... dynamic_enums_json={"foo": ["choice1", "choice2"], "bar": ["optionA", "optionB"]},
... )
**Notes:**
- This function must be executed within a Windmill flow or flow preview.
- The function checks for required environment variables (`WM_FLOW_JOB_ID`, `WM_FLOW_STEP_ID`) to ensure it is run in the appropriate context.
"""
workspace = self.workspace
flow_job_id = os.environ.get("WM_FLOW_JOB_ID")
Expand All @@ -662,8 +693,8 @@ def request_interactive_slack_approval(
params["flow_step_id"] = os.environ.get("WM_FLOW_STEP_ID")
if default_args_json:
params["default_args_json"] = json.dumps(default_args_json)
if dynamic_enum_json:
params["dynamic_enum_json"] = json.dumps(dynamic_enum_json)
if dynamic_enums_json:
params["dynamic_enums_json"] = json.dumps(dynamic_enums_json)

self.get(
f"/w/{workspace}/jobs/slack_approval/{os.environ.get('WM_JOB_ID', 'NO_JOB_ID')}",
Expand Down Expand Up @@ -1026,15 +1057,15 @@ def request_interactive_slack_approval(
message: str = None,
approver: str = None,
default_args_json: dict = None,
dynamic_enum_json: dict = None,
dynamic_enums_json: dict = None,
) -> None:
return _client.request_interactive_slack_approval(
slack_resource_path=slack_resource_path,
channel_id=channel_id,
message=message,
approver=approver,
default_args_json=default_args_json,
dynamic_enum_json=dynamic_enum_json,
dynamic_enums_json=dynamic_enums_json,
)

@init_global_client
Expand Down
43 changes: 38 additions & 5 deletions typescript-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -853,16 +853,49 @@ interface SlackApprovalOptions {
message?: string;
approver?: string;
defaultArgsJson?: Record<string, any>;
dynamicEnumJson?: Record<string, any>;
dynamicEnumsJson?: Record<string, any>;
}

/**
* Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields.
*
* **[Enterprise Edition Only]** To include form fields in the Slack approval request, go to **Advanced -> Suspend -> Form**
* and define a form. Learn more at [Windmill Documentation](https://www.windmill.dev/docs/flows/flow_approval#form).
*
* @param {Object} options - The configuration options for the Slack approval request.
* @param {string} options.slackResourcePath - The path to the Slack resource in Windmill.
* @param {string} options.channelId - The Slack channel ID where the approval request will be sent.
* @param {string} [options.message] - Optional custom message to include in the Slack approval request.
* @param {string} [options.approver] - Optional user ID or name of the approver for the request.
* @param {DefaultArgs} [options.defaultArgsJson] - Optional object defining or overriding the default arguments to a form field.
* @param {Enums} [options.dynamicEnumsJson] - Optional object overriding the enum default values of an enum form field.
*
* @returns {Promise<void>} Resolves when the Slack approval request is successfully sent.
*
* @throws {Error} If the function is not called within a flow or flow preview.
* @throws {Error} If the `JobService.getSlackApprovalPayload` call fails.
*
* **Usage Example:**
* ```typescript
* await requestInteractiveSlackApproval({
* slackResourcePath: "/u/alex/my_slack_resource",
* channelId: "admins-slack-channel",
* message: "Please approve this request",
* approver: "approver123",
* defaultArgsJson: { key1: "value1", key2: 42 },
* dynamicEnumsJson: { foo: ["choice1", "choice2"], bar: ["optionA", "optionB"] },
* });
* ```
*
* **Note:** This function requires execution within a Windmill flow or flow preview.
*/
export async function requestInteractiveSlackApproval({
slackResourcePath,
channelId,
message,
approver,
defaultArgsJson,
dynamicEnumJson,
dynamicEnumsJson,
}: SlackApprovalOptions): Promise<void> {
const workspace = getWorkspace();
const flowJobId = getEnv("WM_FLOW_JOB_ID");
Expand All @@ -886,7 +919,7 @@ export async function requestInteractiveSlackApproval({
channelId: string;
flowStepId: string;
defaultArgsJson?: string;
dynamicEnumJson?: string;
dynamicEnumsJson?: string;
} = {
slackResourcePath,
channelId,
Expand All @@ -904,8 +937,8 @@ export async function requestInteractiveSlackApproval({
params.defaultArgsJson = JSON.stringify(defaultArgsJson);
}

if (dynamicEnumJson) {
params.dynamicEnumJson = JSON.stringify(dynamicEnumJson);
if (dynamicEnumsJson) {
params.dynamicEnumsJson = JSON.stringify(dynamicEnumsJson);
}

await JobService.getSlackApprovalPayload({
Expand Down

0 comments on commit 63b0968

Please sign in to comment.