fix(slack): preserve time colons in interactive labels (#99877)

* fix(slack): preserve time colons in button labels

* fix(slack): preserve clock labels in interactive replies

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
qingminlong
2026-07-04 03:14:13 -07:00
committed by GitHub
co-authored by Peter Steinberger
parent 79d2e568a0
commit bd2740fedc
2 changed files with 122 additions and 1 deletions
@@ -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" },
],
},
],
});
});
});
+25 -1
View File
@@ -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,