Slack: add rich text previews for modal inputs
This commit is contained in:
@@ -707,7 +707,15 @@ describe("registerSlackInteractionEvents", () => {
|
||||
type: "rich_text_input",
|
||||
rich_text_value: {
|
||||
type: "rich_text",
|
||||
elements: [{ type: "rich_text_section", elements: [] }],
|
||||
elements: [
|
||||
{
|
||||
type: "rich_text_section",
|
||||
elements: [
|
||||
{ type: "text", text: "Ship this now" },
|
||||
{ type: "text", text: "with canary metrics" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -736,6 +744,7 @@ describe("registerSlackInteractionEvents", () => {
|
||||
inputEmail?: string;
|
||||
inputUrl?: string;
|
||||
richTextValue?: unknown;
|
||||
richTextPreview?: string;
|
||||
}>;
|
||||
};
|
||||
expect(payload.inputs).toEqual(
|
||||
@@ -791,15 +800,72 @@ describe("registerSlackInteractionEvents", () => {
|
||||
expect.objectContaining({
|
||||
actionId: "richtext_input",
|
||||
inputKind: "rich_text",
|
||||
richTextPreview: "Ship this now with canary metrics",
|
||||
richTextValue: {
|
||||
type: "rich_text",
|
||||
elements: [{ type: "rich_text_section", elements: [] }],
|
||||
elements: [
|
||||
{
|
||||
type: "rich_text_section",
|
||||
elements: [
|
||||
{ type: "text", text: "Ship this now" },
|
||||
{ type: "text", text: "with canary metrics" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("truncates rich text preview to keep payload summaries compact", async () => {
|
||||
enqueueSystemEventMock.mockReset();
|
||||
const { ctx, getViewHandler } = createContext();
|
||||
registerSlackInteractionEvents({ ctx: ctx as never });
|
||||
const viewHandler = getViewHandler();
|
||||
expect(viewHandler).toBeTruthy();
|
||||
|
||||
const longText = "deploy ".repeat(40).trim();
|
||||
const ack = vi.fn().mockResolvedValue(undefined);
|
||||
await viewHandler!({
|
||||
ack,
|
||||
body: {
|
||||
user: { id: "U555" },
|
||||
view: {
|
||||
id: "V555",
|
||||
callback_id: "openclaw:long_richtext",
|
||||
state: {
|
||||
values: {
|
||||
richtext_block: {
|
||||
richtext_input: {
|
||||
type: "rich_text_input",
|
||||
rich_text_value: {
|
||||
type: "rich_text",
|
||||
elements: [
|
||||
{
|
||||
type: "rich_text_section",
|
||||
elements: [{ type: "text", text: longText }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(ack).toHaveBeenCalled();
|
||||
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
|
||||
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
|
||||
inputs: Array<{ actionId: string; richTextPreview?: string }>;
|
||||
};
|
||||
const richInput = payload.inputs.find((input) => input.actionId === "richtext_input");
|
||||
expect(richInput?.richTextPreview).toBeTruthy();
|
||||
expect((richInput?.richTextPreview ?? "").length).toBeLessThanOrEqual(120);
|
||||
});
|
||||
|
||||
it("captures modal close events and enqueues view closed event", async () => {
|
||||
enqueueSystemEventMock.mockReset();
|
||||
const { ctx, getViewClosedHandler, resolveSessionKey } = createContext();
|
||||
|
||||
@@ -38,6 +38,7 @@ type InteractionSummary = {
|
||||
inputEmail?: string;
|
||||
inputUrl?: string;
|
||||
richTextValue?: unknown;
|
||||
richTextPreview?: string;
|
||||
userId?: string;
|
||||
teamId?: string;
|
||||
triggerId?: string;
|
||||
@@ -66,6 +67,7 @@ type ModalInputSummary = {
|
||||
inputEmail?: string;
|
||||
inputUrl?: string;
|
||||
richTextValue?: unknown;
|
||||
richTextPreview?: string;
|
||||
};
|
||||
|
||||
function readOptionValues(options: unknown): string[] | undefined {
|
||||
@@ -107,6 +109,35 @@ function uniqueNonEmptyStrings(values: string[]): string[] {
|
||||
return unique;
|
||||
}
|
||||
|
||||
function collectRichTextFragments(value: unknown, out: string[]): void {
|
||||
if (!value || typeof value !== "object") {
|
||||
return;
|
||||
}
|
||||
const typed = value as { text?: unknown; elements?: unknown };
|
||||
if (typeof typed.text === "string" && typed.text.trim().length > 0) {
|
||||
out.push(typed.text.trim());
|
||||
}
|
||||
if (Array.isArray(typed.elements)) {
|
||||
for (const child of typed.elements) {
|
||||
collectRichTextFragments(child, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeRichTextPreview(value: unknown): string | undefined {
|
||||
const fragments: string[] = [];
|
||||
collectRichTextFragments(value, fragments);
|
||||
if (fragments.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const joined = fragments.join(" ").replace(/\s+/g, " ").trim();
|
||||
if (!joined) {
|
||||
return undefined;
|
||||
}
|
||||
const max = 120;
|
||||
return joined.length <= max ? joined : `${joined.slice(0, max - 1)}…`;
|
||||
}
|
||||
|
||||
function summarizeAction(
|
||||
action: Record<string, unknown>,
|
||||
): Omit<InteractionSummary, "actionId" | "blockId"> {
|
||||
@@ -166,6 +197,7 @@ function summarizeAction(
|
||||
}
|
||||
}
|
||||
const richTextValue = actionType === "rich_text_input" ? typed.rich_text_value : undefined;
|
||||
const richTextPreview = summarizeRichTextPreview(richTextValue);
|
||||
const inputKind =
|
||||
actionType === "number_input"
|
||||
? "number"
|
||||
@@ -197,6 +229,7 @@ function summarizeAction(
|
||||
inputEmail,
|
||||
inputUrl,
|
||||
richTextValue,
|
||||
richTextPreview,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -242,6 +275,9 @@ function formatInteractionSelectionLabel(params: {
|
||||
if (typeof params.summary.selectedDateTime === "number") {
|
||||
return new Date(params.summary.selectedDateTime * 1000).toISOString();
|
||||
}
|
||||
if (params.summary.richTextPreview) {
|
||||
return params.summary.richTextPreview;
|
||||
}
|
||||
if (params.summary.value?.trim()) {
|
||||
return params.summary.value.trim();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user