Slack: expand advanced modal controls payloads and confirms

This commit is contained in:
Colin
2026-02-16 13:37:43 -05:00
committed by Peter Steinberger
parent 5bbbc3e3e6
commit 05ab147081
4 changed files with 68 additions and 10 deletions

View File

@@ -504,6 +504,9 @@ describe("registerSlackInteractionEvents", () => {
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
actionType: string;
selectedValues?: string[];
selectedUsers?: string[];
selectedChannels?: string[];
selectedConversations?: string[];
selectedLabels?: string[];
selectedDate?: string;
selectedTime?: string;
@@ -520,6 +523,9 @@ describe("registerSlackInteractionEvents", () => {
"G777",
"G888",
]);
expect(payload.selectedUsers).toEqual(["U777", "U888"]);
expect(payload.selectedChannels).toEqual(["C777", "C888"]);
expect(payload.selectedConversations).toEqual(["G777", "G888"]);
expect(payload.selectedLabels).toEqual(["Alpha", "Beta"]);
expect(payload.selectedDate).toBe("2026-02-16");
expect(payload.selectedTime).toBe("14:30");
@@ -719,6 +725,9 @@ describe("registerSlackInteractionEvents", () => {
actionId: string;
inputKind?: string;
selectedValues?: string[];
selectedUsers?: string[];
selectedChannels?: string[];
selectedConversations?: string[];
selectedLabels?: string[];
selectedDate?: string;
selectedTime?: string;
@@ -736,9 +745,21 @@ describe("registerSlackInteractionEvents", () => {
selectedValues: ["prod"],
selectedLabels: ["Production"],
}),
expect.objectContaining({ actionId: "assignee_select", selectedValues: ["U900"] }),
expect.objectContaining({ actionId: "channel_select", selectedValues: ["C900"] }),
expect.objectContaining({ actionId: "convo_select", selectedValues: ["G900"] }),
expect.objectContaining({
actionId: "assignee_select",
selectedValues: ["U900"],
selectedUsers: ["U900"],
}),
expect.objectContaining({
actionId: "channel_select",
selectedValues: ["C900"],
selectedChannels: ["C900"],
}),
expect.objectContaining({
actionId: "convo_select",
selectedValues: ["G900"],
selectedConversations: ["G900"],
}),
expect.objectContaining({ actionId: "date_select", selectedDate: "2026-02-16" }),
expect.objectContaining({ actionId: "time_select", selectedTime: "12:45" }),
expect.objectContaining({ actionId: "datetime_select", selectedDateTime: 1_771_632_300 }),

View File

@@ -26,6 +26,9 @@ type InteractionSummary = {
inputKind?: "text" | "number" | "email" | "url" | "rich_text";
value?: string;
selectedValues?: string[];
selectedUsers?: string[];
selectedChannels?: string[];
selectedConversations?: string[];
selectedLabels?: string[];
selectedDate?: string;
selectedTime?: string;
@@ -51,6 +54,9 @@ type ModalInputSummary = {
inputKind?: "text" | "number" | "email" | "url" | "rich_text";
value?: string;
selectedValues?: string[];
selectedUsers?: string[];
selectedChannels?: string[];
selectedConversations?: string[];
selectedLabels?: string[];
selectedDate?: string;
selectedTime?: string;
@@ -121,15 +127,24 @@ function summarizeAction(
rich_text_value?: unknown;
};
const actionType = typed.type;
const selectedUsers = uniqueNonEmptyStrings([
...(typed.selected_user ? [typed.selected_user] : []),
...(Array.isArray(typed.selected_users) ? typed.selected_users : []),
]);
const selectedChannels = uniqueNonEmptyStrings([
...(typed.selected_channel ? [typed.selected_channel] : []),
...(Array.isArray(typed.selected_channels) ? typed.selected_channels : []),
]);
const selectedConversations = uniqueNonEmptyStrings([
...(typed.selected_conversation ? [typed.selected_conversation] : []),
...(Array.isArray(typed.selected_conversations) ? typed.selected_conversations : []),
]);
const selectedValues = uniqueNonEmptyStrings([
...(typed.selected_option?.value ? [typed.selected_option.value] : []),
...(readOptionValues(typed.selected_options) ?? []),
...(typed.selected_user ? [typed.selected_user] : []),
...(Array.isArray(typed.selected_users) ? typed.selected_users : []),
...(typed.selected_channel ? [typed.selected_channel] : []),
...(Array.isArray(typed.selected_channels) ? typed.selected_channels : []),
...(typed.selected_conversation ? [typed.selected_conversation] : []),
...(Array.isArray(typed.selected_conversations) ? typed.selected_conversations : []),
...selectedUsers,
...selectedChannels,
...selectedConversations,
]);
const selectedLabels = uniqueNonEmptyStrings([
...(typed.selected_option?.text?.text ? [typed.selected_option.text.text] : []),
@@ -169,6 +184,9 @@ function summarizeAction(
inputKind,
value: typed.value,
selectedValues: selectedValues.length > 0 ? selectedValues : undefined,
selectedUsers: selectedUsers.length > 0 ? selectedUsers : undefined,
selectedChannels: selectedChannels.length > 0 ? selectedChannels : undefined,
selectedConversations: selectedConversations.length > 0 ? selectedConversations : undefined,
selectedLabels: selectedLabels.length > 0 ? selectedLabels : undefined,
selectedDate: typed.selected_date,
selectedTime: typed.selected_time,

View File

@@ -169,7 +169,7 @@ function encodeValue(parts: { command: string; arg: string; value: string; userI
function findFirstActionsBlock(payload: { blocks?: Array<{ type: string }> }) {
return payload.blocks?.find((block) => block.type === "actions") as
| { type: string; elements?: Array<{ type?: string; action_id?: string }> }
| { type: string; elements?: Array<{ type?: string; action_id?: string; confirm?: unknown }> }
| undefined;
}
@@ -293,6 +293,7 @@ describe("Slack native command argument menus", () => {
const actions = findFirstActionsBlock(payload);
const elementType = actions?.elements?.[0]?.type;
expect(elementType).toBe("button");
expect(actions?.elements?.[0]?.confirm).toBeTruthy();
});
it("shows a static_select menu when choices exceed button row size", async () => {
@@ -321,6 +322,7 @@ describe("Slack native command argument menus", () => {
const element = actions?.elements?.[0];
expect(element?.type).toBe("static_select");
expect(element?.action_id).toBe("openclaw_cmdarg");
expect(element?.confirm).toBeTruthy();
});
it("falls back to buttons when static_select value limit would be exceeded", async () => {
@@ -345,6 +347,7 @@ describe("Slack native command argument menus", () => {
const actions = findFirstActionsBlock(payload);
const firstElement = actions?.elements?.[0];
expect(firstElement?.type).toBe("button");
expect(firstElement?.confirm).toBeTruthy();
});
it("shows an overflow menu when choices fit compact range", async () => {
@@ -370,6 +373,7 @@ describe("Slack native command argument menus", () => {
const element = actions?.elements?.[0];
expect(element?.type).toBe("overflow");
expect(element?.action_id).toBe("openclaw_cmdarg");
expect(element?.confirm).toBeTruthy();
});
it("dispatches the command when a menu button is clicked", async () => {

View File

@@ -47,6 +47,18 @@ function truncatePlainText(value: string, max: number): string {
return `${trimmed.slice(0, max - 1)}`;
}
function buildSlackArgMenuConfirm(params: { command: string; arg: string }) {
return {
title: { type: "plain_text", text: "Confirm selection" },
text: {
type: "mrkdwn",
text: `Run */${params.command}* with *${params.arg}* set to this value?`,
},
confirm: { type: "plain_text", text: "Run command" },
deny: { type: "plain_text", text: "Cancel" },
};
}
type CommandsRegistry = typeof import("../../auto-reply/commands-registry.js");
let commandsRegistry: CommandsRegistry | undefined;
async function getCommandsRegistry(): Promise<CommandsRegistry> {
@@ -141,6 +153,7 @@ function buildSlackCommandArgMenuBlocks(params: {
{
type: "overflow",
action_id: SLACK_COMMAND_ARG_ACTION_ID,
confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }),
options: encodedChoices.map((choice) => ({
text: { type: "plain_text", text: choice.label.slice(0, 75) },
value: choice.value,
@@ -157,6 +170,7 @@ function buildSlackCommandArgMenuBlocks(params: {
action_id: SLACK_COMMAND_ARG_ACTION_ID,
text: { type: "plain_text", text: choice.label },
value: choice.value,
confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }),
})),
}))
: chunkItems(encodedChoices, SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX).map((choices, index) => ({
@@ -165,6 +179,7 @@ function buildSlackCommandArgMenuBlocks(params: {
{
type: "static_select",
action_id: SLACK_COMMAND_ARG_ACTION_ID,
confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }),
placeholder: {
type: "plain_text",
text: index === 0 ? `Choose ${params.arg}` : `Choose ${params.arg} (${index + 1})`,