Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Visit [http://localhost:3000](http://localhost:3000) to get started.
- **AI Gateway**: Generate Text, Generate Image
- **Firecrawl**: Scrape URL, Search Web
- **Linear**: Create Ticket, Find Issues
- **Native**: HTTP Request
- **Resend**: Send Email
- **Slack**: Send Slack Message
- **Superagent**: Guard, Redact
Expand Down
6 changes: 6 additions & 0 deletions app/workflows/[workflowId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
import { NodeConfigPanel } from "@/components/workflow/node-config-panel";
import { useIsMobile } from "@/hooks/use-mobile";
import { api } from "@/lib/api-client";
import { fetchIntegrationsAtom } from "@/lib/integrations-store";
import {
currentWorkflowIdAtom,
currentWorkflowNameAtom,
Expand Down Expand Up @@ -55,6 +56,7 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => {
const setTriggerExecute = useSetAtom(triggerExecuteAtom);
const setRightPanelWidth = useSetAtom(rightPanelWidthAtom);
const setIsPanelAnimating = useSetAtom(isPanelAnimatingAtom);
const fetchIntegrations = useSetAtom(fetchIntegrationsAtom);
const [hasSidebarBeenShown, setHasSidebarBeenShown] = useAtom(
hasSidebarBeenShownAtom
);
Expand Down Expand Up @@ -315,6 +317,9 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => {
const storedPrompt = sessionStorage.getItem("ai-prompt");
const storedWorkflowId = sessionStorage.getItem("generating-workflow-id");

// Prefetch integrations in parallel with workflow loading
fetchIntegrations();

// Check if state is already loaded for this workflow
if (currentWorkflowId === workflowId && nodes.length > 0) {
return;
Expand Down Expand Up @@ -342,6 +347,7 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => {
nodes.length,
generateWorkflowFromAI,
loadExistingWorkflow,
fetchIntegrations,
]);

// Keyboard shortcuts
Expand Down
8 changes: 6 additions & 2 deletions components/settings/integration-form-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,13 @@ const SYSTEM_INTEGRATION_LABELS: Record<string, string> = {
database: "Database",
};

// Get all integration types (plugins + system)
// Get all integration types (plugins that require integration + system)
// Excludes plugins with requiresIntegration: false (like Native)
const getIntegrationTypes = (): IntegrationType[] => [
...getSortedIntegrationTypes(),
...getSortedIntegrationTypes().filter((type) => {
const plugin = getIntegration(type);
return plugin?.requiresIntegration !== false;
}),
...SYSTEM_INTEGRATION_TYPES,
];

Expand Down
55 changes: 30 additions & 25 deletions components/ui/integration-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import { useAtomValue, useSetAtom } from "jotai";
import { AlertTriangle } from "lucide-react";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import {
Select,
SelectContent,
Expand All @@ -10,7 +11,12 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { api, type Integration } from "@/lib/api-client";
import {
fetchIntegrationsAtom,
integrationsAtom,
integrationsFetchedAtom,
integrationsLoadingAtom,
} from "@/lib/integrations-store";
import type { IntegrationType } from "@/lib/types/integration";
import { IntegrationFormDialog } from "@/components/settings/integration-form-dialog";

Expand All @@ -31,32 +37,31 @@ export function IntegrationSelector({
label,
disabled,
}: IntegrationSelectorProps) {
const [integrations, setIntegrations] = useState<Integration[]>([]);
const [loading, setLoading] = useState(true);
const allIntegrations = useAtomValue(integrationsAtom);
const loading = useAtomValue(integrationsLoadingAtom);
const fetched = useAtomValue(integrationsFetchedAtom);
const fetchIntegrations = useSetAtom(fetchIntegrationsAtom);
const [showNewDialog, setShowNewDialog] = useState(false);

const loadIntegrations = async () => {
try {
setLoading(true);
const all = await api.integration.getAll();
const filtered = all.filter((i) => i.type === integrationType);
setIntegrations(filtered);

// Auto-select if only one option and nothing selected yet
if (filtered.length === 1 && !value) {
onChange(filtered[0].id);
}
} catch (error) {
console.error("Failed to load integrations:", error);
} finally {
setLoading(false);
// Filter integrations by type
const integrations = useMemo(
() => allIntegrations.filter((i) => i.type === integrationType),
[allIntegrations, integrationType]
);

// Fetch integrations on mount if not already fetched
useEffect(() => {
if (!fetched && !loading) {
fetchIntegrations();
}
};
}, [fetched, loading, fetchIntegrations]);

// Auto-select if only one option and nothing selected yet
useEffect(() => {
loadIntegrations();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [integrationType]);
if (integrations.length === 1 && !value && fetched) {
onChange(integrations[0].id);
}
}, [integrations, value, fetched, onChange]);

const handleValueChange = (newValue: string) => {
if (newValue === "__new__") {
Expand All @@ -69,12 +74,12 @@ export function IntegrationSelector({
};

const handleNewIntegrationCreated = async (integrationId: string) => {
await loadIntegrations();
await fetchIntegrations();
onChange(integrationId);
setShowNewDialog(false);
};

if (loading) {
if (loading || !fetched) {
return (
<Select disabled value="">
<SelectTrigger className="flex-1">
Expand Down
6 changes: 4 additions & 2 deletions components/ui/template-autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const getNodeDisplayName = (node: WorkflowNode): string => {

if (node.data.type === "action") {
const actionType = node.data.config?.actionType as string | undefined;
return actionType || "HTTP Request";
return actionType || "Action";
}

if (node.data.type === "trigger") {
Expand Down Expand Up @@ -122,7 +122,9 @@ const getCommonFields = (node: WorkflowNode) => {
{ field: "number", description: "Ticket number" },
];
}
if (actionType === "HTTP Request") {
if (
isActionType(actionType, "HTTP Request", "native/http-request")
) {
return [
{ field: "data", description: "Response data" },
{ field: "status", description: "HTTP status code" },
Expand Down
19 changes: 16 additions & 3 deletions components/ui/template-badge-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface TemplateBadgeInputProps {
disabled?: boolean;
className?: string;
id?: string;
/** Optional non-editable prefix to display before the input */
prefix?: string;
}

// Helper to check if a template references an existing node
Expand Down Expand Up @@ -71,6 +73,7 @@ export function TemplateBadgeInput({
disabled,
className,
id,
prefix,
}: TemplateBadgeInputProps) {
const [isFocused, setIsFocused] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -487,12 +490,17 @@ export function TemplateBadgeInput({
document.execCommand("insertText", false, text);
};

// Trigger display update when placeholder changes (e.g., when integration prefix changes)
useEffect(() => {
shouldUpdateDisplay.current = true;
}, [placeholder]);

// Update display only when needed (not while typing)
useEffect(() => {
if (shouldUpdateDisplay.current) {
updateDisplay();
}
}, [internalValue, isFocused]);
}, [internalValue, isFocused, placeholder]);

return (
<>
Expand All @@ -503,8 +511,13 @@ export function TemplateBadgeInput({
className
)}
>
{prefix && (
<span className="text-muted-foreground flex-shrink-0 select-none pr-1 font-mono text-xs leading-[1.35rem]">
{prefix}
</span>
)}
<div
className="w-full outline-none"
className="w-full overflow-hidden whitespace-nowrap outline-none"
contentEditable={!disabled}
id={id}
onBlur={handleBlur}
Expand All @@ -516,7 +529,7 @@ export function TemplateBadgeInput({
suppressContentEditableWarning
/>
</div>

<TemplateAutocomplete
currentNodeId={selectedNodeId || undefined}
filter={autocompleteFilter}
Expand Down
Loading