Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Visit [http://localhost:3000](http://localhost:3000) to get started.
- **Firecrawl**: Scrape URL, Search Web
- **GitHub**: Create Issue, List Issues, Get Issue, Update Issue
- **Linear**: Create Ticket, Find Issues
- **Native**: HTTP Request
- **Perplexity**: Search Web, Ask Question, Research Topic
- **Resend**: Send Email
- **Slack**: Send Slack Message
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 {
integrationsAtom,
integrationsLoadedAtom,
Expand Down Expand Up @@ -122,6 +123,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 @@ -391,6 +393,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 @@ -418,6 +423,7 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => {
nodes.length,
generateWorkflowFromAI,
loadExistingWorkflow,
fetchIntegrations,
]);

// Auto-fix invalid/missing integrations on workflow load or when integrations change
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 @@ -46,9 +46,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
34 changes: 28 additions & 6 deletions components/ui/integration-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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 @@ -11,6 +11,11 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import {
fetchIntegrationsAtom,
integrationsAtom,
integrationsFetchedAtom,
integrationsLoadingAtom,
import { api, type Integration } from "@/lib/api-client";
import {
integrationsAtom,
Expand All @@ -36,13 +41,25 @@ 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 integrationsVersion = useAtomValue(integrationsVersionAtom);
const setGlobalIntegrations = useSetAtom(integrationsAtom);
const setIntegrationsVersion = useSetAtom(integrationsVersionAtom);

// 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();
const loadIntegrations = async () => {
try {
setLoading(true);
Expand All @@ -61,9 +78,14 @@ export function IntegrationSelector({
} finally {
setLoading(false);
}
};
}, [fetched, loading, fetchIntegrations]);

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

const handleNewIntegrationCreated = async (integrationId: string) => {
await loadIntegrations();
await fetchIntegrations();
onChange(integrationId);
setShowNewDialog(false);
// Increment version to trigger auto-fix for other nodes that need this integration type
setIntegrationsVersion((v) => v + 1);
};

if (loading) {
if (loading || !fetched) {
return (
<Select disabled value="">
<SelectTrigger className="flex-1">
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 @@ -14,6 +14,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 @@ -80,6 +82,7 @@ export function TemplateBadgeInput({
disabled,
className,
id,
prefix,
}: TemplateBadgeInputProps) {
const [isFocused, setIsFocused] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -496,12 +499,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 @@ -512,8 +520,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 @@ -525,7 +538,7 @@ export function TemplateBadgeInput({
suppressContentEditableWarning
/>
</div>

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