mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
Support routine variables in titles
This commit is contained in:
parent
372421ef0b
commit
1de5fb9316
7 changed files with 41 additions and 17 deletions
|
|
@ -12,9 +12,18 @@ describe("routine variable helpers", () => {
|
|||
).toEqual(["repo", "priority"]);
|
||||
});
|
||||
|
||||
it("deduplicates placeholder names across the routine title and description", () => {
|
||||
expect(
|
||||
extractRoutineVariableNames([
|
||||
"Triage {{repo}}",
|
||||
"Review {{repo}} for {{priority}} bugs",
|
||||
]),
|
||||
).toEqual(["repo", "priority"]);
|
||||
});
|
||||
|
||||
it("preserves existing metadata when syncing variables from a template", () => {
|
||||
expect(
|
||||
syncRoutineVariablesWithTemplate("Review {{repo}} and {{priority}}", [
|
||||
syncRoutineVariablesWithTemplate(["Triage {{repo}}", "Review {{repo}} and {{priority}}"], [
|
||||
{ name: "repo", label: "Repository", type: "text", defaultValue: "paperclip", required: true, options: [] },
|
||||
]),
|
||||
).toEqual([
|
||||
|
|
|
|||
|
|
@ -1,18 +1,25 @@
|
|||
import type { RoutineVariable } from "./types/routine.js";
|
||||
|
||||
const ROUTINE_VARIABLE_MATCHER = /\{\{\s*([A-Za-z][A-Za-z0-9_]*)\s*\}\}/g;
|
||||
type RoutineTemplateInput = string | null | undefined | Array<string | null | undefined>;
|
||||
|
||||
export function isValidRoutineVariableName(name: string): boolean {
|
||||
return /^[A-Za-z][A-Za-z0-9_]*$/.test(name);
|
||||
}
|
||||
|
||||
export function extractRoutineVariableNames(template: string | null | undefined): string[] {
|
||||
if (!template) return [];
|
||||
function normalizeRoutineTemplateInput(input: RoutineTemplateInput): string[] {
|
||||
const templates = Array.isArray(input) ? input : [input];
|
||||
return templates.filter((template): template is string => typeof template === "string" && template.length > 0);
|
||||
}
|
||||
|
||||
export function extractRoutineVariableNames(template: RoutineTemplateInput): string[] {
|
||||
const found = new Set<string>();
|
||||
for (const match of template.matchAll(ROUTINE_VARIABLE_MATCHER)) {
|
||||
const name = match[1];
|
||||
if (name && !found.has(name)) {
|
||||
found.add(name);
|
||||
for (const source of normalizeRoutineTemplateInput(template)) {
|
||||
for (const match of source.matchAll(ROUTINE_VARIABLE_MATCHER)) {
|
||||
const name = match[1];
|
||||
if (name && !found.has(name)) {
|
||||
found.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...found];
|
||||
|
|
@ -30,7 +37,7 @@ function defaultRoutineVariable(name: string): RoutineVariable {
|
|||
}
|
||||
|
||||
export function syncRoutineVariablesWithTemplate(
|
||||
template: string | null | undefined,
|
||||
template: RoutineTemplateInput,
|
||||
existing: RoutineVariable[] | null | undefined,
|
||||
): RoutineVariable[] {
|
||||
const names = extractRoutineVariableNames(template);
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => {
|
|||
projectId,
|
||||
goalId: null,
|
||||
parentIssueId: null,
|
||||
title: "repo triage",
|
||||
title: "repo triage for {{repo}}",
|
||||
description: "Review {{repo}} for {{priority}} bugs",
|
||||
assigneeAgentId: agentId,
|
||||
priority: "medium",
|
||||
|
|
@ -346,6 +346,7 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => {
|
|||
},
|
||||
{},
|
||||
);
|
||||
expect(variableRoutine.variables.map((variable) => variable.name)).toEqual(["repo", "priority"]);
|
||||
|
||||
const run = await svc.runRoutine(variableRoutine.id, {
|
||||
source: "manual",
|
||||
|
|
@ -353,7 +354,7 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => {
|
|||
});
|
||||
|
||||
const storedIssue = await db
|
||||
.select({ description: issues.description })
|
||||
.select({ title: issues.title, description: issues.description })
|
||||
.from(issues)
|
||||
.where(eq(issues.id, run.linkedIssueId!))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
|
|
@ -363,6 +364,7 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => {
|
|||
.where(eq(routineRuns.id, run.id))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
|
||||
expect(storedIssue?.title).toBe("repo triage for paperclip");
|
||||
expect(storedIssue?.description).toBe("Review paperclip for high bugs");
|
||||
expect(storedRun?.triggerPayload).toEqual({
|
||||
variables: {
|
||||
|
|
|
|||
|
|
@ -675,6 +675,7 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup
|
|||
executionWorkspaceSettings?: Record<string, unknown> | null;
|
||||
}) {
|
||||
const resolvedVariables = resolveRoutineVariableValues(input.routine.variables ?? [], input);
|
||||
const title = interpolateRoutineTemplate(input.routine.title, resolvedVariables) ?? input.routine.title;
|
||||
const description = interpolateRoutineTemplate(input.routine.description, resolvedVariables);
|
||||
const triggerPayload = mergeRoutineRunPayload(input.payload, resolvedVariables);
|
||||
const run = await db.transaction(async (tx) => {
|
||||
|
|
@ -748,7 +749,7 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup
|
|||
projectId: input.routine.projectId,
|
||||
goalId: input.routine.goalId,
|
||||
parentId: input.routine.parentIssueId,
|
||||
title: input.routine.title,
|
||||
title,
|
||||
description,
|
||||
status: "todo",
|
||||
priority: input.routine.priority,
|
||||
|
|
@ -996,7 +997,7 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup
|
|||
if (input.goalId) await assertGoal(companyId, input.goalId);
|
||||
if (input.parentIssueId) await assertParentIssue(companyId, input.parentIssueId);
|
||||
const variables = syncRoutineVariablesWithTemplate(
|
||||
input.description,
|
||||
[input.title, input.description],
|
||||
sanitizeRoutineVariableInputs(input.variables),
|
||||
);
|
||||
assertRoutineVariableDefinitions(variables);
|
||||
|
|
@ -1029,9 +1030,10 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup
|
|||
if (!existing) return null;
|
||||
const nextProjectId = patch.projectId ?? existing.projectId;
|
||||
const nextAssigneeAgentId = patch.assigneeAgentId ?? existing.assigneeAgentId;
|
||||
const nextTitle = patch.title ?? existing.title;
|
||||
const nextDescription = patch.description === undefined ? existing.description : patch.description;
|
||||
const nextVariables = syncRoutineVariablesWithTemplate(
|
||||
nextDescription,
|
||||
[nextTitle, nextDescription],
|
||||
patch.variables === undefined ? existing.variables : sanitizeRoutineVariableInputs(patch.variables),
|
||||
);
|
||||
if (patch.projectId) await assertProject(existing.companyId, nextProjectId);
|
||||
|
|
@ -1060,7 +1062,7 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup
|
|||
projectId: nextProjectId,
|
||||
goalId: patch.goalId === undefined ? existing.goalId : patch.goalId,
|
||||
parentIssueId: patch.parentIssueId === undefined ? existing.parentIssueId : patch.parentIssueId,
|
||||
title: patch.title ?? existing.title,
|
||||
title: nextTitle,
|
||||
description: nextDescription,
|
||||
assigneeAgentId: nextAssigneeAgentId,
|
||||
priority: patch.priority ?? existing.priority,
|
||||
|
|
|
|||
|
|
@ -36,18 +36,20 @@ function updateVariableList(
|
|||
}
|
||||
|
||||
export function RoutineVariablesEditor({
|
||||
title,
|
||||
description,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
value: RoutineVariable[];
|
||||
onChange: (value: RoutineVariable[]) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(true);
|
||||
const syncedVariables = useMemo(
|
||||
() => syncRoutineVariablesWithTemplate(description, value),
|
||||
[description, value],
|
||||
() => syncRoutineVariablesWithTemplate([title, description], value),
|
||||
[description, title, value],
|
||||
);
|
||||
const syncedSignature = serializeVariables(syncedVariables);
|
||||
const currentSignature = serializeVariables(value);
|
||||
|
|
@ -68,7 +70,7 @@ export function RoutineVariablesEditor({
|
|||
<div>
|
||||
<p className="text-sm font-medium">Variables</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Detected from `{"{{name}}"}` placeholders in the routine instructions.
|
||||
Detected from `{"{{name}}"}` placeholders in the routine title and instructions.
|
||||
</p>
|
||||
</div>
|
||||
{open ? <ChevronDown className="h-4 w-4 text-muted-foreground" /> : <ChevronRight className="h-4 w-4 text-muted-foreground" />}
|
||||
|
|
|
|||
|
|
@ -860,6 +860,7 @@ export function RoutineDetail() {
|
|||
/>
|
||||
<RoutineVariablesHint />
|
||||
<RoutineVariablesEditor
|
||||
title={editDraft.title}
|
||||
description={editDraft.description}
|
||||
value={editDraft.variables}
|
||||
onChange={(variables) => setEditDraft((current) => ({ ...current, variables }))}
|
||||
|
|
|
|||
|
|
@ -806,6 +806,7 @@ export function Routines() {
|
|||
<div className="mt-3 space-y-3">
|
||||
<RoutineVariablesHint />
|
||||
<RoutineVariablesEditor
|
||||
title={draft.title}
|
||||
description={draft.description}
|
||||
value={draft.variables}
|
||||
onChange={(variables) => setDraft((current) => ({ ...current, variables }))}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue