Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

interactive slack approvals: more improvements / nits #5012

Merged
merged 2 commits into from
Jan 4, 2025
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 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