mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 09:01:22 +00:00
Slack: add overflow menus for slash arg choices
This commit is contained in:
@@ -4,6 +4,7 @@ import { getSlackSlashMocks, resetSlackSlashMocks } from "./slash.test-harness.j
|
|||||||
vi.mock("../../auto-reply/commands-registry.js", () => {
|
vi.mock("../../auto-reply/commands-registry.js", () => {
|
||||||
const usageCommand = { key: "usage", nativeName: "usage" };
|
const usageCommand = { key: "usage", nativeName: "usage" };
|
||||||
const reportCommand = { key: "report", nativeName: "report" };
|
const reportCommand = { key: "report", nativeName: "report" };
|
||||||
|
const reportCompactCommand = { key: "reportcompact", nativeName: "reportcompact" };
|
||||||
const reportLongCommand = { key: "reportlong", nativeName: "reportlong" };
|
const reportLongCommand = { key: "reportlong", nativeName: "reportlong" };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -31,6 +32,9 @@ vi.mock("../../auto-reply/commands-registry.js", () => {
|
|||||||
if (normalized === "report") {
|
if (normalized === "report") {
|
||||||
return reportCommand;
|
return reportCommand;
|
||||||
}
|
}
|
||||||
|
if (normalized === "reportcompact") {
|
||||||
|
return reportCompactCommand;
|
||||||
|
}
|
||||||
if (normalized === "reportlong") {
|
if (normalized === "reportlong") {
|
||||||
return reportLongCommand;
|
return reportLongCommand;
|
||||||
}
|
}
|
||||||
@@ -49,6 +53,12 @@ vi.mock("../../auto-reply/commands-registry.js", () => {
|
|||||||
acceptsArgs: true,
|
acceptsArgs: true,
|
||||||
args: [],
|
args: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "reportcompact",
|
||||||
|
description: "ReportCompact",
|
||||||
|
acceptsArgs: true,
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "reportlong",
|
name: "reportlong",
|
||||||
description: "ReportLong",
|
description: "ReportLong",
|
||||||
@@ -95,6 +105,21 @@ vi.mock("../../auto-reply/commands-registry.js", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (params.command?.key === "reportcompact") {
|
||||||
|
const values = (params.args?.values ?? {}) as Record<string, unknown>;
|
||||||
|
if (typeof values.period === "string" && values.period.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
arg: { name: "period", description: "period" },
|
||||||
|
choices: [
|
||||||
|
{ value: "day", label: "day" },
|
||||||
|
{ value: "week", label: "week" },
|
||||||
|
{ value: "month", label: "month" },
|
||||||
|
{ value: "quarter", label: "quarter" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
if (params.command?.key !== "usage") {
|
if (params.command?.key !== "usage") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -197,6 +222,7 @@ describe("Slack native command argument menus", () => {
|
|||||||
let harness: ReturnType<typeof createArgMenusHarness>;
|
let harness: ReturnType<typeof createArgMenusHarness>;
|
||||||
let usageHandler: (args: unknown) => Promise<void>;
|
let usageHandler: (args: unknown) => Promise<void>;
|
||||||
let reportHandler: (args: unknown) => Promise<void>;
|
let reportHandler: (args: unknown) => Promise<void>;
|
||||||
|
let reportCompactHandler: (args: unknown) => Promise<void>;
|
||||||
let reportLongHandler: (args: unknown) => Promise<void>;
|
let reportLongHandler: (args: unknown) => Promise<void>;
|
||||||
let argMenuHandler: (args: unknown) => Promise<void>;
|
let argMenuHandler: (args: unknown) => Promise<void>;
|
||||||
|
|
||||||
@@ -214,6 +240,11 @@ describe("Slack native command argument menus", () => {
|
|||||||
throw new Error("Missing /report handler");
|
throw new Error("Missing /report handler");
|
||||||
}
|
}
|
||||||
reportHandler = report;
|
reportHandler = report;
|
||||||
|
const reportCompact = harness.commands.get("/reportcompact");
|
||||||
|
if (!reportCompact) {
|
||||||
|
throw new Error("Missing /reportcompact handler");
|
||||||
|
}
|
||||||
|
reportCompactHandler = reportCompact;
|
||||||
const reportLong = harness.commands.get("/reportlong");
|
const reportLong = harness.commands.get("/reportlong");
|
||||||
if (!reportLong) {
|
if (!reportLong) {
|
||||||
throw new Error("Missing /reportlong handler");
|
throw new Error("Missing /reportlong handler");
|
||||||
@@ -311,6 +342,33 @@ describe("Slack native command argument menus", () => {
|
|||||||
expect(firstElement?.type).toBe("button");
|
expect(firstElement?.type).toBe("button");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows an overflow menu when choices fit compact range", async () => {
|
||||||
|
const respond = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const ack = vi.fn().mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await reportCompactHandler({
|
||||||
|
command: {
|
||||||
|
user_id: "U1",
|
||||||
|
user_name: "Ada",
|
||||||
|
channel_id: "C1",
|
||||||
|
channel_name: "directmessage",
|
||||||
|
text: "",
|
||||||
|
trigger_id: "t1",
|
||||||
|
},
|
||||||
|
ack,
|
||||||
|
respond,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(respond).toHaveBeenCalledTimes(1);
|
||||||
|
const payload = respond.mock.calls[0]?.[0] as { blocks?: Array<{ type: string }> };
|
||||||
|
expect(payload.blocks?.[1]?.type).toBe("actions");
|
||||||
|
const element = (
|
||||||
|
payload.blocks?.[1] as { elements?: Array<{ type?: string; action_id?: string }> } | undefined
|
||||||
|
)?.elements?.[0];
|
||||||
|
expect(element?.type).toBe("overflow");
|
||||||
|
expect(element?.action_id).toBe("openclaw_cmdarg");
|
||||||
|
});
|
||||||
|
|
||||||
it("dispatches the command when a menu button is clicked", async () => {
|
it("dispatches the command when a menu button is clicked", async () => {
|
||||||
const respond = vi.fn().mockResolvedValue(undefined);
|
const respond = vi.fn().mockResolvedValue(undefined);
|
||||||
await argMenuHandler({
|
await argMenuHandler({
|
||||||
@@ -353,6 +411,33 @@ describe("Slack native command argument menus", () => {
|
|||||||
expect(call.ctx?.Body).toBe("/report month");
|
expect(call.ctx?.Body).toBe("/report month");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("dispatches the command when an overflow option is chosen", async () => {
|
||||||
|
const respond = vi.fn().mockResolvedValue(undefined);
|
||||||
|
await argMenuHandler({
|
||||||
|
ack: vi.fn().mockResolvedValue(undefined),
|
||||||
|
action: {
|
||||||
|
selected_option: {
|
||||||
|
value: encodeValue({
|
||||||
|
command: "reportcompact",
|
||||||
|
arg: "period",
|
||||||
|
value: "quarter",
|
||||||
|
userId: "U1",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
user: { id: "U1", name: "Ada" },
|
||||||
|
channel: { id: "C1", name: "directmessage" },
|
||||||
|
trigger_id: "t1",
|
||||||
|
},
|
||||||
|
respond,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(dispatchMock).toHaveBeenCalledTimes(1);
|
||||||
|
const call = dispatchMock.mock.calls[0]?.[0] as { ctx?: { Body?: string } };
|
||||||
|
expect(call.ctx?.Body).toBe("/reportcompact quarter");
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects menu clicks from other users", async () => {
|
it("rejects menu clicks from other users", async () => {
|
||||||
const respond = vi.fn().mockResolvedValue(undefined);
|
const respond = vi.fn().mockResolvedValue(undefined);
|
||||||
await argMenuHandler({
|
await argMenuHandler({
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ type SlackBlock = { type: string; [key: string]: unknown };
|
|||||||
const SLACK_COMMAND_ARG_ACTION_ID = "openclaw_cmdarg";
|
const SLACK_COMMAND_ARG_ACTION_ID = "openclaw_cmdarg";
|
||||||
const SLACK_COMMAND_ARG_VALUE_PREFIX = "cmdarg";
|
const SLACK_COMMAND_ARG_VALUE_PREFIX = "cmdarg";
|
||||||
const SLACK_COMMAND_ARG_BUTTON_ROW_SIZE = 5;
|
const SLACK_COMMAND_ARG_BUTTON_ROW_SIZE = 5;
|
||||||
|
const SLACK_COMMAND_ARG_OVERFLOW_MIN = 3;
|
||||||
|
const SLACK_COMMAND_ARG_OVERFLOW_MAX = 5;
|
||||||
const SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX = 100;
|
const SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX = 100;
|
||||||
const SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX = 75;
|
const SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX = 75;
|
||||||
|
|
||||||
@@ -115,8 +117,27 @@ function buildSlackCommandArgMenuBlocks(params: {
|
|||||||
const canUseStaticSelect = encodedChoices.every(
|
const canUseStaticSelect = encodedChoices.every(
|
||||||
(choice) => choice.value.length <= SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX,
|
(choice) => choice.value.length <= SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX,
|
||||||
);
|
);
|
||||||
const rows =
|
const canUseOverflow =
|
||||||
encodedChoices.length <= SLACK_COMMAND_ARG_BUTTON_ROW_SIZE || !canUseStaticSelect
|
canUseStaticSelect &&
|
||||||
|
encodedChoices.length >= SLACK_COMMAND_ARG_OVERFLOW_MIN &&
|
||||||
|
encodedChoices.length <= SLACK_COMMAND_ARG_OVERFLOW_MAX;
|
||||||
|
const rows = canUseOverflow
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: "actions",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: "overflow",
|
||||||
|
action_id: SLACK_COMMAND_ARG_ACTION_ID,
|
||||||
|
options: encodedChoices.map((choice) => ({
|
||||||
|
text: { type: "plain_text", text: choice.label.slice(0, 75) },
|
||||||
|
value: choice.value,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: encodedChoices.length <= SLACK_COMMAND_ARG_BUTTON_ROW_SIZE || !canUseStaticSelect
|
||||||
? chunkItems(encodedChoices, SLACK_COMMAND_ARG_BUTTON_ROW_SIZE).map((choices) => ({
|
? chunkItems(encodedChoices, SLACK_COMMAND_ARG_BUTTON_ROW_SIZE).map((choices) => ({
|
||||||
type: "actions",
|
type: "actions",
|
||||||
elements: choices.map((choice) => ({
|
elements: choices.map((choice) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user