Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/workflows/[workflowId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,7 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => {
{/* Hover indicator */}
<div className="absolute inset-y-0 left-0 w-1 bg-transparent transition-colors group-hover:bg-blue-500 group-active:bg-blue-600" />
{/* Collapse button - hidden while resizing */}
{!isDraggingResize && !panelCollapsed && (
{!(isDraggingResize || panelCollapsed) && (
<button
className="-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-0 flex size-6 items-center justify-center rounded-full border bg-background opacity-0 shadow-sm transition-opacity hover:bg-muted group-hover:opacity-100"
onClick={(e) => {
Expand Down
93 changes: 4 additions & 89 deletions components/workflow/config/action-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,80 +26,6 @@ type ActionConfigProps = {
disabled: boolean;
};

// Find Issues fields component (kept hardcoded - Linear plugin incomplete)
function FindIssuesFields({
config,
onUpdateConfig,
disabled,
}: {
config: Record<string, unknown>;
onUpdateConfig: (key: string, value: string) => void;
disabled: boolean;
}) {
return (
<>
<div className="space-y-2">
<Label className="ml-1" htmlFor="linearAssigneeId">
Assignee (User ID)
</Label>
<TemplateBadgeInput
disabled={disabled}
id="linearAssigneeId"
onChange={(value) => onUpdateConfig("linearAssigneeId", value)}
placeholder="user-id-123 or {{NodeName.userId}}"
value={(config?.linearAssigneeId as string) || ""}
/>
</div>
<div className="space-y-2">
<Label className="ml-1" htmlFor="linearTeamId">
Team ID (optional)
</Label>
<TemplateBadgeInput
disabled={disabled}
id="linearTeamId"
onChange={(value) => onUpdateConfig("linearTeamId", value)}
placeholder="team-id-456 or {{NodeName.teamId}}"
value={(config?.linearTeamId as string) || ""}
/>
</div>
<div className="space-y-2">
<Label className="ml-1" htmlFor="linearStatus">
Status (optional)
</Label>
<Select
disabled={disabled}
onValueChange={(value) => onUpdateConfig("linearStatus", value)}
value={(config?.linearStatus as string) || "any"}
>
<SelectTrigger className="w-full" id="linearStatus">
<SelectValue placeholder="Any status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="any">Any</SelectItem>
<SelectItem value="backlog">Backlog</SelectItem>
<SelectItem value="todo">Todo</SelectItem>
<SelectItem value="in_progress">In Progress</SelectItem>
<SelectItem value="done">Done</SelectItem>
<SelectItem value="canceled">Canceled</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="ml-1" htmlFor="linearLabel">
Label (optional)
</Label>
<TemplateBadgeInput
disabled={disabled}
id="linearLabel"
onChange={(value) => onUpdateConfig("linearLabel", value)}
placeholder="bug, feature, etc. or {{NodeName.label}}"
value={(config?.linearLabel as string) || ""}
/>
</div>
</>
);
}

// Database Query fields component
function DatabaseQueryFields({
config,
Expand Down Expand Up @@ -435,25 +361,14 @@ export function ActionConfig({
/>
)}

{/* Find Issues - kept hardcoded (Linear plugin incomplete) */}
{config?.actionType === "Find Issues" && (
<FindIssuesFields
{/* Plugin actions - dynamic config fields */}
{pluginAction && !SYSTEM_ACTIONS.includes(actionType) && (
<pluginAction.configFields
config={config}
disabled={disabled}
onUpdateConfig={onUpdateConfig}
onUpdateConfig={handlePluginUpdateConfig}
/>
)}

{/* Plugin actions - dynamic config fields */}
{pluginAction &&
!SYSTEM_ACTIONS.includes(actionType) &&
actionType !== "Find Issues" && (
<pluginAction.configFields
config={config}
disabled={disabled}
onUpdateConfig={handlePluginUpdateConfig}
/>
)}
</>
);
}
56 changes: 56 additions & 0 deletions plugins/linear/codegen/find-issues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Code generation template for Find Issues action
* Used when exporting workflows to standalone Next.js projects
*/
export const findIssuesCodegenTemplate = `import { LinearClient } from '@linear/sdk';

export async function findIssuesStep(input: {
linearAssigneeId?: string;
linearTeamId?: string;
linearStatus?: string;
linearLabel?: string;
}) {
"use step";

const linear = new LinearClient({ apiKey: process.env.LINEAR_API_KEY });

const filter: Record<string, unknown> = {};

if (input.linearAssigneeId) {
filter.assignee = { id: { eq: input.linearAssigneeId } };
}

if (input.linearTeamId) {
filter.team = { id: { eq: input.linearTeamId } };
}

if (input.linearStatus && input.linearStatus !== 'any') {
filter.state = { name: { eqIgnoreCase: input.linearStatus } };
}

if (input.linearLabel) {
filter.labels = { name: { eqIgnoreCase: input.linearLabel } };
}

const issues = await linear.issues({ filter });

const mappedIssues = await Promise.all(
issues.nodes.map(async (issue) => {
const state = await issue.state;
return {
id: issue.id,
title: issue.title,
url: issue.url,
state: state?.name || 'Unknown',
priority: issue.priority,
assigneeId: issue.assigneeId || undefined,
};
})
);

return {
issues: mappedIssues,
count: mappedIssues.length,
};
}`;

15 changes: 8 additions & 7 deletions plugins/linear/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Ticket } from "lucide-react";
import { Search, Ticket } from "lucide-react";
import type { IntegrationPlugin } from "../registry";
import { registerIntegration } from "../registry";
import { createTicketCodegenTemplate } from "./codegen/create-ticket";
import { findIssuesCodegenTemplate } from "./codegen/find-issues";
import { LinearSettings } from "./settings";
import { CreateTicketConfigFields } from "./steps/create-ticket/config";
import { FindIssuesConfigFields } from "./steps/find-issues/config";

const linearPlugin: IntegrationPlugin = {
type: "linear",
Expand Down Expand Up @@ -70,17 +72,16 @@ const linearPlugin: IntegrationPlugin = {
configFields: CreateTicketConfigFields,
codegenTemplate: createTicketCodegenTemplate,
},
// TODO: Add Find Issues action
{
id: "Find Issues",
label: "Find Issues",
description: "Search for issues in Linear",
category: "Linear",
icon: Ticket,
stepFunction: "createTicketStep", // TODO: Implement separate findIssuesStep
stepImportPath: "create-ticket",
configFields: CreateTicketConfigFields,
codegenTemplate: createTicketCodegenTemplate,
icon: Search,
stepFunction: "findIssuesStep",
stepImportPath: "find-issues",
configFields: FindIssuesConfigFields,
codegenTemplate: findIssuesCodegenTemplate,
},
],
};
Expand Down
87 changes: 87 additions & 0 deletions plugins/linear/steps/find-issues/config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { TemplateBadgeInput } from "@/components/ui/template-badge-input";

/**
* Find Issues Config Fields Component
* UI for configuring the find issues action
*/
export function FindIssuesConfigFields({
config,
onUpdateConfig,
disabled,
}: {
config: Record<string, unknown>;
onUpdateConfig: (key: string, value: unknown) => void;
disabled?: boolean;
}) {
return (
<>
<div className="space-y-2">
<Label className="ml-1" htmlFor="linearAssigneeId">
Assignee (User ID)
</Label>
<TemplateBadgeInput
disabled={disabled}
id="linearAssigneeId"
onChange={(value) => onUpdateConfig("linearAssigneeId", value)}
placeholder="user-id-123 or {{NodeName.userId}}"
value={(config?.linearAssigneeId as string) || ""}
/>
</div>
<div className="space-y-2">
<Label className="ml-1" htmlFor="linearTeamId">
Team ID (optional)
</Label>
<TemplateBadgeInput
disabled={disabled}
id="linearTeamId"
onChange={(value) => onUpdateConfig("linearTeamId", value)}
placeholder="team-id-456 or {{NodeName.teamId}}"
value={(config?.linearTeamId as string) || ""}
/>
</div>
<div className="space-y-2">
<Label className="ml-1" htmlFor="linearStatus">
Status (optional)
</Label>
<Select
disabled={disabled}
onValueChange={(value) => onUpdateConfig("linearStatus", value)}
value={(config?.linearStatus as string) || "any"}
>
<SelectTrigger className="w-full" id="linearStatus">
<SelectValue placeholder="Any status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="any">Any</SelectItem>
<SelectItem value="backlog">Backlog</SelectItem>
<SelectItem value="todo">Todo</SelectItem>
<SelectItem value="in_progress">In Progress</SelectItem>
<SelectItem value="done">Done</SelectItem>
<SelectItem value="canceled">Canceled</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="ml-1" htmlFor="linearLabel">
Label (optional)
</Label>
<TemplateBadgeInput
disabled={disabled}
id="linearLabel"
onChange={(value) => onUpdateConfig("linearLabel", value)}
placeholder="bug, feature, etc. or {{NodeName.label}}"
value={(config?.linearLabel as string) || ""}
/>
</div>
</>
);
}

97 changes: 97 additions & 0 deletions plugins/linear/steps/find-issues/step.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import "server-only";

import { LinearClient } from "@linear/sdk";
import { fetchCredentials } from "@/lib/credential-fetcher";
import { getErrorMessage } from "@/lib/utils";

type LinearIssue = {
id: string;
title: string;
url: string;
state: string;
priority: number;
assigneeId?: string;
};

type FindIssuesResult =
| { success: true; issues: LinearIssue[]; count: number }
| { success: false; error: string };

/**
* Find Issues Step
* Searches for issues in Linear based on filters
*/
export async function findIssuesStep(input: {
integrationId?: string;
linearAssigneeId?: string;
linearTeamId?: string;
linearStatus?: string;
linearLabel?: string;
}): Promise<FindIssuesResult> {
"use step";

const credentials = input.integrationId
? await fetchCredentials(input.integrationId)
: {};

const apiKey = credentials.LINEAR_API_KEY;

if (!apiKey) {
return {
success: false,
error:
"LINEAR_API_KEY is not configured. Please add it in Project Integrations.",
};
}

try {
const linear = new LinearClient({ apiKey });

// Build filter object
const filter: Record<string, unknown> = {};

if (input.linearAssigneeId) {
filter.assignee = { id: { eq: input.linearAssigneeId } };
}

if (input.linearTeamId) {
filter.team = { id: { eq: input.linearTeamId } };
}

if (input.linearStatus && input.linearStatus !== "any") {
filter.state = { name: { eqIgnoreCase: input.linearStatus } };
}

if (input.linearLabel) {
filter.labels = { name: { eqIgnoreCase: input.linearLabel } };
}

const issues = await linear.issues({ filter });

const mappedIssues: LinearIssue[] = await Promise.all(
issues.nodes.map(async (issue) => {
const state = await issue.state;
return {
id: issue.id,
title: issue.title,
url: issue.url,
state: state?.name || "Unknown",
priority: issue.priority,
assigneeId: issue.assigneeId || undefined,
};
})
);

return {
success: true,
issues: mappedIssues,
count: mappedIssues.length,
};
} catch (error) {
return {
success: false,
error: `Failed to find issues: ${getErrorMessage(error)}`,
};
}
}