Polish workflow index labels

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-08-14 00:17:35 -07:00
parent d52292870e
commit 37f288942b
7 changed files with 559 additions and 46 deletions
@@ -20,8 +20,12 @@ import {
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { useCustomEmoji } from "@/features/custom-emoji/hooks";
import { reactionEmojiUrl } from "@/shared/api/customEmoji";
import type { Workflow } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
import { emojiDisplayName } from "@/shared/lib/emojiName";
import { rewriteRelayUrl } from "@/shared/lib/mediaUrl";
import { Button } from "@/shared/ui/button";
import {
DropdownMenu,
@@ -34,9 +38,13 @@ import {
getWorkflowDescription,
getWorkflowDisplayStatus,
getWorkflowPrimaryAction,
getWorkflowPrimaryActionChannel,
getWorkflowPrimaryActionEmoji,
getWorkflowTriggerConfig,
getWorkflowTriggerSummary,
getWorkflowTriggerType,
} from "./workflowDefinition";
import { useWorkflowTriggerPresentation } from "./useWorkflowTriggerPresentation";
type WorkflowCardProps = {
workflow: Workflow;
@@ -89,6 +97,87 @@ function StatusBadge({ status }: { status: Workflow["status"] }) {
);
}
function ReactionGlyph({
className,
emoji,
testId,
url,
}: {
className: string;
emoji: string;
testId: string;
url?: string;
}) {
return url ? (
<img
alt={emoji}
className={cn("inline-block object-contain", className)}
data-testid={testId}
draggable={false}
src={rewriteRelayUrl(url)}
title={emojiDisplayName(emoji)}
/>
) : (
<span
className={cn(
"inline-flex items-center justify-center leading-none",
emoji.startsWith(":") && "text-3xs",
className,
)}
data-testid={testId}
title={emojiDisplayName(emoji)}
>
{emoji}
</span>
);
}
function ReactionLabelText({
reactions,
text,
}: {
reactions: ReadonlyArray<{ emoji: string; url?: string }>;
text: string;
}) {
const candidates = [...reactions].sort(
(left, right) => right.emoji.length - left.emoji.length,
);
const parts: React.ReactNode[] = [];
let cursor = 0;
while (cursor < text.length) {
let nextIndex = -1;
let nextReaction: (typeof candidates)[number] | undefined;
for (const reaction of candidates) {
const matchIndex = text.indexOf(reaction.emoji, cursor);
if (matchIndex >= 0 && (nextIndex < 0 || matchIndex < nextIndex)) {
nextIndex = matchIndex;
nextReaction = reaction;
}
}
if (!nextReaction) break;
if (nextIndex > cursor) parts.push(text.slice(cursor, nextIndex));
parts.push(
nextReaction.url ? (
<ReactionGlyph
className="h-5 w-5 align-text-bottom"
emoji={nextReaction.emoji}
key={`${nextReaction.emoji}-${nextIndex}`}
testId="workflow-card-reaction-emoji"
url={nextReaction.url}
/>
) : (
nextReaction.emoji
),
);
cursor = nextIndex + nextReaction.emoji.length;
}
if (cursor < text.length) parts.push(text.slice(cursor));
return parts.length > 0 ? parts : text;
}
export function WorkflowCard({
workflow,
channelName,
@@ -98,14 +187,54 @@ export function WorkflowCard({
onDuplicate,
onDelete,
}: WorkflowCardProps) {
const customEmoji = useCustomEmoji();
const displayStatus = getWorkflowDisplayStatus(workflow);
const description = getWorkflowDescription(workflow.definition);
const triggerSummary = getWorkflowTriggerSummary(workflow.definition);
const configuredTrigger = getWorkflowTriggerConfig(workflow.definition);
const trigger = configuredTrigger ?? { on: "message_posted" as const };
const triggerPresentation = useWorkflowTriggerPresentation({
trigger,
workflowChannelId: workflow.channelId,
});
const triggerSummary =
configuredTrigger &&
["diff_posted", "message_posted", "reaction_added"].includes(trigger.on)
? triggerPresentation.description
: getWorkflowTriggerSummary(workflow.definition);
const triggerType = getWorkflowTriggerType(workflow.definition);
const actionType = getWorkflowPrimaryAction(workflow.definition);
const cardLabel = getWorkflowCardLabel(workflow.definition);
const actionChannel = getWorkflowPrimaryActionChannel(workflow.definition);
const actionReaction = getWorkflowPrimaryActionEmoji(workflow.definition);
const actionChannelLabel =
channelName && (!actionChannel || actionChannel === workflow.channelId)
? channelName
: undefined;
const cardLabel = getWorkflowCardLabel(workflow.definition, {
actionChannelLabel,
triggerDescription:
configuredTrigger &&
triggerPresentation.description !==
getWorkflowTriggerSummary({ trigger: { on: configuredTrigger.on } })
? triggerPresentation.description
: undefined,
triggerReaction: triggerPresentation.emoji,
});
const cardReactions = [
...new Set([triggerPresentation.emoji, actionReaction]),
]
.filter((emoji): emoji is string => Boolean(emoji))
.map((emoji) => ({
emoji,
url: reactionEmojiUrl(emoji, customEmoji),
}));
const TriggerIcon = triggerType ? TRIGGER_ICONS[triggerType] : undefined;
const ActionIcon = actionType ? ACTION_ICONS[actionType] : undefined;
const triggerReaction = cardReactions.find(
({ emoji }) => emoji === triggerPresentation.emoji,
);
const actionReactionPresentation = cardReactions.find(
({ emoji }) => emoji === actionReaction,
);
const theme = triggerType ? TRIGGER_THEMES[triggerType] : undefined;
return (
@@ -134,7 +263,14 @@ export function WorkflowCard({
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2" aria-hidden="true">
<span className="flex h-9 w-9 items-center justify-center rounded-xl bg-white/15 shadow-xs ring-1 ring-white/15">
{TriggerIcon ? (
{triggerReaction ? (
<ReactionGlyph
className="h-6 w-6 text-2xl"
emoji={triggerReaction.emoji}
testId="workflow-card-trigger-reaction"
url={triggerReaction.url}
/>
) : TriggerIcon ? (
<TriggerIcon className="h-5 w-5" />
) : (
<Zap className="h-5 w-5" />
@@ -144,7 +280,16 @@ export function WorkflowCard({
<>
<ArrowRight className="h-4 w-4 text-white/60" />
<span className="flex h-9 w-9 items-center justify-center rounded-xl bg-white/15 shadow-xs ring-1 ring-white/15">
<ActionIcon className="h-5 w-5" />
{actionReactionPresentation ? (
<ReactionGlyph
className="h-6 w-6 text-2xl"
emoji={actionReactionPresentation.emoji}
testId="workflow-card-action-reaction"
url={actionReactionPresentation.url}
/>
) : (
<ActionIcon className="h-5 w-5" />
)}
</span>
</>
) : null}
@@ -190,11 +335,14 @@ export function WorkflowCard({
{triggerSummary ? (
<p className="mt-4 line-clamp-1 text-xs font-semibold text-white/70">
{triggerSummary}
<ReactionLabelText
reactions={cardReactions}
text={triggerSummary}
/>
</p>
) : null}
<h3 className="mt-1 line-clamp-4 text-xl font-bold leading-tight tracking-tight">
{cardLabel}
<ReactionLabelText reactions={cardReactions} text={cardLabel} />
</h3>
{description ? (
<p className="mt-2 line-clamp-2 text-sm leading-relaxed text-white/75">
@@ -1,15 +1,18 @@
import assert from "node:assert/strict";
import test from "node:test";
import { getWorkflowCardLabel } from "./workflowDefinition.ts";
import {
getWorkflowCardLabel,
getWorkflowPrimaryActionEmoji,
} from "./workflowDefinition.ts";
test("builds a plain-language workflow card label", () => {
assert.equal(
getWorkflowCardLabel({
trigger: { on: "message_posted" },
steps: [{ action: "send_message" }],
steps: [{ action: "send_message", text: "Deploying now" }],
}),
"When a message is posted, send a channel message",
"When a message is posted, send “Deploying now”",
);
assert.equal(
@@ -22,6 +25,90 @@ test("builds a plain-language workflow card label", () => {
}),
"When someone reacts with 🔥, wait 5m, then 1 more step",
);
assert.equal(
getWorkflowCardLabel(
{
trigger: {
on: "reaction_added",
filter: 'trigger_emoji == "🫠"',
},
steps: [{ action: "add_reaction", emoji: "👍" }],
},
{ triggerReaction: "🫠" },
),
"When someone reacts with 🫠, add a 👍 reaction",
);
assert.equal(
getWorkflowCardLabel(
{
trigger: { on: "message_posted" },
steps: [{ action: "send_message", text: "Deploying now" }],
},
{ actionChannelLabel: "releases" },
),
"When a message is posted, send “Deploying now” in #releases",
);
assert.equal(
getWorkflowCardLabel(
{
trigger: {
on: "message_posted",
filter: `trigger_author == "${"a".repeat(64)}"`,
},
steps: [{ action: "send_message", text: "Deploying now" }],
},
{ triggerDescription: "Message posted by Carl" },
),
"When a message is posted by Carl, send “Deploying now”",
);
assert.equal(
getWorkflowCardLabel(
{
trigger: {
on: "message_posted",
filter: 'str_contains(trigger_text, "deploy")',
},
steps: [{ action: "call_webhook" }],
},
{ triggerDescription: "Message contains “deploy”" },
),
"When a message contains “deploy”, call a webhook",
);
assert.equal(
getWorkflowCardLabel(
{
trigger: {
on: "message_posted",
filter: `trigger_text == "FUCK" && trigger_author == "${"a".repeat(64)}"`,
},
steps: [{ action: "send_message", text: "{{trigger.text}} yourself" }],
},
{
triggerDescription: "Message “FUCK” is posted by Carl",
},
),
"When “FUCK” is posted by Carl, send “{{trigger.text}} yourself”",
);
});
test("returns the primary add-reaction emoji for rich card rendering", () => {
assert.equal(
getWorkflowPrimaryActionEmoji({
steps: [{ action: "add_reaction", emoji: ":blob-wave:" }],
}),
":blob-wave:",
);
assert.equal(
getWorkflowPrimaryActionEmoji({
steps: [{ action: "send_message", emoji: ":blob-wave:" }],
}),
null,
);
});
test("summarizes common and custom schedules", () => {
@@ -3,9 +3,18 @@ import {
scheduleFormFromTrigger,
SCHEDULE_FREQUENCY_LABELS,
} from "./workflowSchedule";
import { ACTION_LABELS, TRIGGER_LABELS } from "./workflowFormTypes";
import type { ActionType } from "./workflowFormTypes";
import type { TriggerType } from "./workflowFormTypes";
import {
ACTION_LABELS,
TRIGGER_LABELS,
TRIGGER_TYPES,
} from "./workflowFormTypes";
import type {
ActionType,
StepFormState,
TriggerConfig,
TriggerType,
} from "./workflowFormTypes";
import { workflowStepDescription } from "./workflowStepDescription";
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -44,6 +53,41 @@ export function getWorkflowPrimaryAction(
return nonEmptyString(getWorkflowSteps(definition)[0]?.action);
}
export function getWorkflowPrimaryActionChannel(
definition: Record<string, unknown>,
): string | null {
return nonEmptyString(getWorkflowSteps(definition)[0]?.channel);
}
export function getWorkflowPrimaryActionEmoji(
definition: Record<string, unknown>,
): string | null {
const step = getWorkflowSteps(definition)[0];
return step?.action === "add_reaction" ? nonEmptyString(step.emoji) : null;
}
export function getWorkflowTriggerConfig(
definition: Record<string, unknown>,
): TriggerConfig | null {
const trigger = asRecord(definition.trigger);
const triggerType = nonEmptyString(trigger?.on);
if (
!trigger ||
!triggerType ||
!TRIGGER_TYPES.includes(triggerType as TriggerType)
) {
return null;
}
return {
on: triggerType as TriggerType,
filter: nonEmptyString(trigger.filter) ?? undefined,
emoji: nonEmptyString(trigger.emoji) ?? undefined,
cron: nonEmptyString(trigger.cron) ?? undefined,
interval: nonEmptyString(trigger.interval) ?? undefined,
};
}
function getScheduleCardClause(trigger: Record<string, unknown>): string {
const schedule = scheduleFormFromTrigger({
on: "schedule",
@@ -69,7 +113,10 @@ function getScheduleCardClause(trigger: Record<string, unknown>): string {
}
}
function getTriggerCardClause(definition: Record<string, unknown>): string {
function getTriggerCardClause(
definition: Record<string, unknown>,
presentedReaction?: string,
): string {
const trigger = asRecord(definition.trigger);
const triggerType = nonEmptyString(trigger?.on);
if (!trigger || !triggerType) return "When this workflow starts";
@@ -80,7 +127,7 @@ function getTriggerCardClause(definition: Record<string, unknown>): string {
? "When a matching message is posted"
: "When a message is posted";
case "reaction_added": {
const emoji = nonEmptyString(trigger.emoji);
const emoji = nonEmptyString(presentedReaction ?? trigger.emoji);
return emoji
? `When someone reacts with ${emoji}`
: "When someone adds a reaction";
@@ -98,32 +145,187 @@ function getTriggerCardClause(definition: Record<string, unknown>): string {
}
}
function getActionCardClause(step: Record<string, unknown>): string | null {
function eventDescriptionCardClause(
description: string,
event: "message" | "diff",
): string | null {
const subject = event === "message" ? "Message" : "Diff";
const eventPhrase = `${subject} posted`;
if (!description.startsWith(eventPhrase)) return null;
const detail = description.slice(eventPhrase.length);
if (!detail) return `When a ${event} is posted`;
if (detail.startsWith(" is ") || detail.startsWith(" is not ")) {
return `When a ${event}${detail}`;
}
if (
detail.startsWith(" containing ") ||
detail.startsWith(" without ") ||
detail.startsWith(" starting with ") ||
detail.startsWith(" ending with ") ||
detail === " with text"
) {
return `When a ${event}${detail} is posted`;
}
return `When a ${event} is posted${detail}`;
}
function textDescriptionCardClause(
description: string,
event: "message" | "diff",
): string | null {
const subject = event === "message" ? "Message" : "Diff";
if (!description.startsWith(`${subject} `)) return null;
return `When a ${event}${description.slice(subject.length)}`;
}
function postedValueCardClause(description: string): string | null {
const postedIndex = description.indexOf(" posted");
if (postedIndex < 1) return null;
const value = description.slice(0, postedIndex);
if (!value.startsWith("“") && !value.startsWith("Anything except ")) {
return null;
}
const normalizedValue = value.startsWith("Anything")
? `anything${value.slice("Anything".length)}`
: value;
return `When ${normalizedValue} is posted${description.slice(postedIndex + " posted".length)}`;
}
function qualifiedPostedEventCardClause(
description: string,
event: "message" | "diff",
): string | null {
const subject = event === "message" ? "Message" : "Diff";
const postedIndex = description.indexOf(" posted");
if (postedIndex < 1) return null;
const suffix = description.slice(postedIndex + " posted".length);
const otherTextPrefix = `${subject} with text other than `;
if (description.startsWith(otherTextPrefix)) {
const value = description.slice(otherTextPrefix.length, postedIndex);
return `When a ${event} with text other than ${value} is posted${suffix}`;
}
if (description.startsWith(`${subject} with text posted`)) {
return `When a ${event} with text is posted${suffix}`;
}
if (description.startsWith(`${subject} without text posted`)) {
return `When a ${event} without text is posted${suffix}`;
}
return null;
}
function exactValueEventCardClause(
description: string,
event: "message" | "diff",
): string | null {
const subject = event === "message" ? "Message" : "Diff";
const prefix = `${subject} “`;
const postedMarker = " is posted";
if (!description.startsWith(prefix)) return null;
const postedIndex = description.indexOf(postedMarker);
if (postedIndex < prefix.length) return null;
const value = description.slice(subject.length + 1, postedIndex);
const suffix = description.slice(postedIndex + postedMarker.length);
return event === "message"
? `When ${value} is posted${suffix}`
: `When a diff ${value} is posted${suffix}`;
}
function getPresentedTriggerCardClause(description?: string): string | null {
if (!description) return null;
const eventClause =
exactValueEventCardClause(description, "message") ??
exactValueEventCardClause(description, "diff") ??
qualifiedPostedEventCardClause(description, "message") ??
qualifiedPostedEventCardClause(description, "diff") ??
postedValueCardClause(description) ??
eventDescriptionCardClause(description, "message") ??
eventDescriptionCardClause(description, "diff") ??
textDescriptionCardClause(description, "message") ??
textDescriptionCardClause(description, "diff");
if (eventClause) return eventClause;
if (description.startsWith("Reaction added")) {
return `When a reaction is added${description.slice("Reaction added".length)}`;
}
if (
description.startsWith("Any reaction ") &&
description.endsWith(" added")
) {
return `When ${description.slice(0, -" added".length).toLocaleLowerCase()} is added`;
}
return null;
}
function getActionCardClause(
step: Record<string, unknown>,
channelLabel?: string,
): string | null {
const action = nonEmptyString(step.action);
if (!action) return null;
const actionLabel = ACTION_LABELS[action as ActionType];
const parsedStep: StepFormState | null = actionLabel
? {
action: action as ActionType,
id: nonEmptyString(step.id) ?? "step_1",
duration: nonEmptyString(step.duration) ?? undefined,
emoji: nonEmptyString(step.emoji) ?? undefined,
from: nonEmptyString(step.from) ?? undefined,
message: nonEmptyString(step.message) ?? undefined,
method: nonEmptyString(step.method) ?? undefined,
name: nonEmptyString(step.name) ?? undefined,
text: nonEmptyString(step.text) ?? undefined,
to: nonEmptyString(step.to) ?? undefined,
topic: nonEmptyString(step.topic) ?? undefined,
url: nonEmptyString(step.url) ?? undefined,
}
: null;
const detail = parsedStep
? workflowStepDescription(parsedStep, {
channelLabel,
includeName: false,
})
: null;
const configuredDetail = detail && detail !== actionLabel ? detail : null;
switch (action) {
case "delay": {
const duration = nonEmptyString(step.duration);
return duration ? `wait ${duration}` : "wait for a moment";
return configuredDetail
? `wait ${configuredDetail}`
: "wait for a moment";
}
case "send_message":
return "send a channel message";
if (!configuredDetail) return "send a channel message";
return nonEmptyString(step.text)
? `send ${configuredDetail}`
: `send a message in ${configuredDetail}`;
case "call_webhook":
return "call a webhook";
return configuredDetail ? `call ${configuredDetail}` : "call a webhook";
case "send_dm":
return "send a direct message";
return configuredDetail
? `send ${configuredDetail}`
: "send a direct message";
case "request_approval":
return "request approval";
return configuredDetail
? `request approval: ${configuredDetail}`
: "request approval";
case "add_reaction": {
const emoji = nonEmptyString(step.emoji);
return emoji ? `add a ${emoji} reaction` : "add a reaction";
return configuredDetail
? `add a ${configuredDetail} reaction`
: "add a reaction";
}
case "set_channel_topic":
return "update the channel topic";
return configuredDetail
? `set the channel topic to ${configuredDetail}`
: "update the channel topic";
default: {
const knownLabel = ACTION_LABELS[action as ActionType];
return (knownLabel ?? humanizeIdentifier(action)).toLocaleLowerCase();
return (actionLabel ?? humanizeIdentifier(action)).toLocaleLowerCase();
}
}
}
@@ -131,10 +333,19 @@ function getActionCardClause(step: Record<string, unknown>): string | null {
/** Build a short plain-language label from the workflow's trigger and steps. */
export function getWorkflowCardLabel(
definition: Record<string, unknown>,
options: {
actionChannelLabel?: string;
triggerDescription?: string;
triggerReaction?: string;
} = {},
): string {
const triggerClause = getTriggerCardClause(definition);
const triggerClause =
getPresentedTriggerCardClause(options.triggerDescription) ??
getTriggerCardClause(definition, options.triggerReaction);
const steps = getWorkflowSteps(definition);
const firstAction = steps[0] ? getActionCardClause(steps[0]) : null;
const firstAction = steps[0]
? getActionCardClause(steps[0], options.actionChannelLabel)
: null;
if (!firstAction) return triggerClause;
const remainingStepCount = steps.length - 1;
@@ -84,3 +84,18 @@ test("falls back to the action label until a step is configured", () => {
"Send message",
);
});
test("can omit a custom step name when composing an index label", () => {
assert.equal(
workflowStepDescription(
{
id: "message",
action: "send_message",
name: "say hello",
text: "hey yourself",
},
{ includeName: false },
),
"“hey yourself”",
);
});
@@ -64,9 +64,9 @@ function configuredStepDetail(
/** Build the concise action summary rendered on a workflow step node. */
export function workflowStepDescription(
step: StepFormState,
options: { channelLabel?: string } = {},
options: { channelLabel?: string; includeName?: boolean } = {},
): string {
const name = step.name?.trim();
const name = options.includeName === false ? undefined : step.name?.trim();
const detail = configuredStepDetail(step, options.channelLabel);
if (name && detail) return `${name} · ${detail}`;
return name || detail || ACTION_LABELS[step.action];
@@ -20,7 +20,31 @@ test("describes selected trigger conditions on the workflow canvas", () => {
on: "message_posted",
filter: 'str_contains(trigger_text, "deploy")',
}),
"Message posted containing “deploy”",
"Message contains “deploy”",
);
assert.equal(
workflowTriggerDescription({
on: "message_posted",
filter: '!str_contains(trigger_text, "deploy")',
}),
"Message doesn’t contain “deploy”",
);
assert.equal(
workflowTriggerDescription({
on: "message_posted",
filter: 'str_starts_with(trigger_text, "deploy")',
}),
"Message starts with “deploy”",
);
assert.equal(
workflowTriggerDescription({
on: "message_posted",
filter: 'str_ends_with(trigger_text, "done")',
}),
"Message ends with “done”",
);
assert.equal(
@@ -28,7 +52,7 @@ test("describes selected trigger conditions on the workflow canvas", () => {
on: "message_posted",
filter: 'trigger_text == "deploy"',
}),
"Message posted is “deploy”",
"Message “deploy” is posted",
);
assert.equal(
@@ -36,7 +60,29 @@ test("describes selected trigger conditions on the workflow canvas", () => {
on: "message_posted",
filter: 'trigger_text != "deploy"',
}),
"Message posted is not “deploy”",
"Message with text other than “deploy” posted",
);
assert.equal(
workflowTriggerDescription(
{
on: "message_posted",
filter: `trigger_text == "FUCK" && trigger_author == "${"a".repeat(64)}"`,
},
{ authorLabel: "Carl" },
),
"Message “FUCK” is posted by Carl",
);
assert.equal(
workflowTriggerDescription(
{
on: "message_posted",
filter: `str_contains(trigger_text, "deploy") && trigger_author == "${"a".repeat(64)}"`,
},
{ authorLabel: "Carl" },
),
"Message by Carl contains “deploy”",
);
assert.equal(
@@ -44,7 +90,7 @@ test("describes selected trigger conditions on the workflow canvas", () => {
on: "message_posted",
filter: "str_len(trigger_text) == 0",
}),
"Message posted without text",
"Message without text posted",
);
assert.equal(
@@ -52,7 +98,7 @@ test("describes selected trigger conditions on the workflow canvas", () => {
on: "message_posted",
filter: "str_len(trigger_text) > 0",
}),
"Messaged posted with text",
"Message with text posted",
);
assert.equal(
@@ -34,26 +34,25 @@ function textConditionDescription(
eventPhrase: string,
condition: ParsedConditionExpression,
): string {
const subject = eventPhrase.replace(/ posted$/, "");
const value = quotedValue(condition.value);
switch (condition.operator) {
case "contains":
return `${eventPhrase} containing ${value}`;
return `${subject} contains ${value}`;
case "not_contains":
return `${eventPhrase} without ${value}`;
return `${subject} doesn’t contain ${value}`;
case "starts_with":
return `${eventPhrase} starting with ${value}`;
return `${subject} starts with ${value}`;
case "ends_with":
return `${eventPhrase} ending with ${value}`;
return `${subject} ends with ${value}`;
case "equals":
return `${eventPhrase} is ${value}`;
return `${subject} ${value} is posted`;
case "not_equals":
return `${eventPhrase} is not ${value}`;
return `${subject} with text other than ${value} posted`;
case "is_not_empty":
return eventPhrase === "Message posted"
? "Messaged posted with text"
: `${eventPhrase} containing text`;
return `${subject} with text posted`;
case "is_empty":
return `${eventPhrase} without text`;
return `${subject} without text posted`;
}
}
@@ -107,10 +106,17 @@ export function workflowTriggerDescription(
if (authorCondition) {
const author =
options.authorLabel ?? truncatePubkey(authorCondition.value);
description +=
const attribution =
authorCondition.operator === "not_equals"
? ` by anyone except ${author}`
: ` by ${author}`;
const subject = eventPhrase.replace(/ posted$/, "");
description =
textCondition &&
description.startsWith(subject) &&
!/ (?:is )?posted$/.test(description)
? `${subject}${attribution}${description.slice(subject.length)}`
: `${description}${attribution}`;
}
if (messageCondition) {
const message = messageReference(