diff --git a/extensions/slack/src/interactive-replies.test.ts b/extensions/slack/src/interactive-replies.test.ts index aac2c460d869..decd5fb34aa2 100644 --- a/extensions/slack/src/interactive-replies.test.ts +++ b/extensions/slack/src/interactive-replies.test.ts @@ -94,4 +94,101 @@ describe("compileSlackInteractiveReplies", () => { ); expect(result.interactive).toBeUndefined(); }); + + it("keeps time-style colons in Slack button labels", () => { + const result = compileSlackInteractiveReplies({ + text: "[[slack_buttons: Fr 10.07. 9:00:slot_fr_0900, Mo 13.07. 10:45:slot_mo_1045, Today 11:30:ticket:123, Mon 14:30-16:00:slot_range]]", + }); + + expect(result.interactive).toEqual({ + blocks: [ + { + type: "buttons", + buttons: [ + { label: "Fr 10.07. 9:00", value: "slot_fr_0900" }, + { label: "Mo 13.07. 10:45", value: "slot_mo_1045" }, + { label: "Today 11:30", value: "ticket:123" }, + { label: "Mon 14:30-16:00", value: "slot_range" }, + ], + }, + ], + }); + }); + + it("keeps button style suffixes after time-style labels", () => { + const result = compileSlackInteractiveReplies({ + text: "[[slack_buttons: Fr 10.07. 9:00:slot_fr_0900:primary, Later 10:45:slot_later:danger]]", + }); + + expect(result.interactive).toEqual({ + blocks: [ + { + type: "buttons", + buttons: [ + { label: "Fr 10.07. 9:00", value: "slot_fr_0900", style: "primary" }, + { label: "Later 10:45", value: "slot_later", style: "danger" }, + ], + }, + ], + }); + }); + + it("preserves colon-containing Slack callback values", () => { + const result = compileSlackInteractiveReplies({ + text: "[[slack_buttons: Model v2:01:open, Step 2:30-day:open, Timed:ticket:9:00:id, Today 11:30:12:34:id, Deny:/approve plugin:approval-123 deny]]", + }); + + expect(result.interactive).toEqual({ + blocks: [ + { + type: "buttons", + buttons: [ + { label: "Model v2", value: "01:open" }, + { label: "Step 2", value: "30-day:open" }, + { label: "Timed", value: "ticket:9:00:id" }, + { label: "Today 11:30", value: "12:34:id" }, + { label: "Deny", value: "/approve plugin:approval-123 deny" }, + ], + }, + ], + }); + }); + + it("keeps single-colon button entries unchanged when the value matches a style name", () => { + const result = compileSlackInteractiveReplies({ + text: "[[slack_buttons: Primary:primary, Danger:danger, Fr 10.07. 9:00:primary]]", + }); + + expect(result.interactive).toEqual({ + blocks: [ + { + type: "buttons", + buttons: [ + { label: "Primary", value: "primary" }, + { label: "Danger", value: "danger" }, + { label: "Fr 10.07. 9:00", value: "primary" }, + ], + }, + ], + }); + }); + + it("keeps time-style colons in Slack select option labels", () => { + const result = compileSlackInteractiveReplies({ + text: "[[slack_select: Pick a time | Fr 10.07. 9:00:slot_fr_0900, Mo 13.07. 10:45:slot_mo_1045]]", + }); + + expect(result.interactive).toEqual({ + blocks: [ + { + type: "select", + placeholder: "Pick a time", + options: [ + { label: "Fr 10.07. 9:00", value: "slot_fr_0900" }, + { label: "Mo 13.07. 10:45", value: "slot_mo_1045" }, + ], + }, + ], + }); + }); }); diff --git a/extensions/slack/src/interactive-replies.ts b/extensions/slack/src/interactive-replies.ts index 06f9ac6c26ac..c4359fdf9842 100644 --- a/extensions/slack/src/interactive-replies.ts +++ b/extensions/slack/src/interactive-replies.ts @@ -21,12 +21,36 @@ type SlackChoice = { style?: "primary" | "secondary" | "success" | "danger"; }; +function resolveChoiceDelimiter(value: string): number { + const firstDelimiter = value.indexOf(":"); + if (firstDelimiter === -1) { + return -1; + } + const prefix = value.slice(0, firstDelimiter); + const suffix = value.slice(firstDelimiter + 1); + const startsWithClockLabel = /\b(?:[01]?\d|2[0-3])$/.test(prefix) && /^[0-5]\d/.test(suffix); + if (!startsWithClockLabel) { + return firstDelimiter; + } + + // Legacy syntax makes label and callback colons ambiguous. Recognize only a + // complete clock or dash-separated range followed by the real separator/end. + const clockEnd = firstDelimiter + 3; + const labelSuffix = + value.slice(clockEnd).match(/^(?:\s*[-–—]\s*(?:[01]?\d|2[0-3]):[0-5]\d)*\s*/)?.[0] ?? ""; + const delimiter = clockEnd + labelSuffix.length; + if (value[delimiter] === ":") { + return delimiter; + } + return delimiter === value.length ? -1 : firstDelimiter; +} + function parseChoice(raw: string, options?: { allowStyle?: boolean }): SlackChoice | null { const trimmed = raw.trim(); if (!trimmed) { return null; } - const delimiter = trimmed.indexOf(":"); + const delimiter = resolveChoiceDelimiter(trimmed); if (delimiter === -1) { return { label: trimmed,