diff --git a/components/settings/integration-form-dialog.tsx b/components/settings/integration-form-dialog.tsx index 5cf37e18..8757af09 100644 --- a/components/settings/integration-form-dialog.tsx +++ b/components/settings/integration-form-dialog.tsx @@ -43,6 +43,7 @@ import { getIntegrationLabels, getSortedIntegrationTypes, } from "@/plugins"; +import { getIntegrationDescriptions } from "@/plugins/registry"; type IntegrationFormDialogProps = { open: boolean; @@ -65,6 +66,9 @@ const SYSTEM_INTEGRATION_TYPES: IntegrationType[] = ["database"]; const SYSTEM_INTEGRATION_LABELS: Record = { database: "Database", }; +const SYSTEM_INTEGRATION_DESCRIPTIONS: Record = { + database: "Connect to PostgreSQL databases", +}; // Get all integration types (plugins + system) const getIntegrationTypes = (): IntegrationType[] => [ @@ -76,6 +80,12 @@ const getIntegrationTypes = (): IntegrationType[] => [ const getLabel = (type: IntegrationType): string => getIntegrationLabels()[type] || SYSTEM_INTEGRATION_LABELS[type] || type; +// Get description for any integration type +const getDescription = (type: IntegrationType): string => + getIntegrationDescriptions()[type] || + SYSTEM_INTEGRATION_DESCRIPTIONS[type] || + ""; + function SecretField({ fieldId, label, @@ -419,20 +429,31 @@ function TypeSelector({ No services found

) : ( - filteredTypes.map((type) => ( - - )) + filteredTypes.map((type) => { + const description = getDescription(type); + return ( + + ); + }) )} diff --git a/components/settings/integrations-dialog.tsx b/components/settings/integrations-dialog.tsx index f992f4f7..1104b4d7 100644 --- a/components/settings/integrations-dialog.tsx +++ b/components/settings/integrations-dialog.tsx @@ -1,7 +1,7 @@ "use client"; import { useSetAtom } from "jotai"; -import { Plus } from "lucide-react"; +import { Plus, Search } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { @@ -12,6 +12,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/spinner"; import { integrationsVersionAtom } from "@/lib/integrations-store"; import { IntegrationsManager } from "./integrations-manager"; @@ -27,6 +28,7 @@ export function IntegrationsDialog({ }: IntegrationsDialogProps) { const [loading, setLoading] = useState(true); const [showCreateDialog, setShowCreateDialog] = useState(false); + const [filter, setFilter] = useState(""); const setIntegrationsVersion = useSetAtom(integrationsVersionAtom); // Track if any changes were made during this dialog session const hasChangesRef = useRef(false); @@ -48,6 +50,8 @@ export function IntegrationsDialog({ hasChangesRef.current = false; // Reset create dialog state when opening setShowCreateDialog(false); + // Reset filter when opening + setFilter(""); } }, [open, loadAll]); @@ -68,10 +72,7 @@ export function IntegrationsDialog({ return ( - + Connections @@ -84,12 +85,24 @@ export function IntegrationsDialog({ ) : ( -
- setShowCreateDialog(false)} - onIntegrationChange={handleIntegrationChange} - showCreateDialog={showCreateDialog} - /> +
+
+ + setFilter(e.target.value)} + placeholder="Filter connections..." + value={filter} + /> +
+
+ setShowCreateDialog(false)} + onIntegrationChange={handleIntegrationChange} + showCreateDialog={showCreateDialog} + /> +
)} diff --git a/components/settings/integrations-manager.tsx b/components/settings/integrations-manager.tsx index 39510676..f6beaca9 100644 --- a/components/settings/integrations-manager.tsx +++ b/components/settings/integrations-manager.tsx @@ -29,12 +29,14 @@ type IntegrationsManagerProps = { showCreateDialog: boolean; onCreateDialogClose?: () => void; onIntegrationChange?: () => void; + filter?: string; }; export function IntegrationsManager({ showCreateDialog: externalShowCreateDialog, onCreateDialogClose, onIntegrationChange, + filter = "", }: IntegrationsManagerProps) { const [integrations, setIntegrations] = useState([]); const [loading, setLoading] = useState(true); @@ -69,6 +71,7 @@ export function IntegrationsManager({ // Get integrations with their labels, sorted by label then name const integrationsWithLabels = useMemo(() => { const labels = getIntegrationLabels() as Record; + const filterLower = filter.toLowerCase(); return integrations .map((integration) => ({ @@ -78,6 +81,14 @@ export function IntegrationsManager({ SYSTEM_INTEGRATION_LABELS[integration.type] || integration.type, })) + .filter((integration) => { + if (!filter) return true; + return ( + integration.label.toLowerCase().includes(filterLower) || + integration.name.toLowerCase().includes(filterLower) || + integration.type.toLowerCase().includes(filterLower) + ); + }) .sort((a, b) => { const labelCompare = a.label.localeCompare(b.label); if (labelCompare !== 0) { @@ -85,7 +96,7 @@ export function IntegrationsManager({ } return a.name.localeCompare(b.name); }); - }, [integrations]); + }, [integrations, filter]); const handleDelete = async (id: string) => { try { @@ -139,70 +150,88 @@ export function IntegrationsManager({ ); } - return ( -
- {integrations.length === 0 ? ( + const renderIntegrationsList = () => { + if (integrations.length === 0) { + return (

No connections configured yet

- ) : ( -
- {integrationsWithLabels.map((integration) => ( -
-
- - {integration.label} - - {integration.name} - -
-
- - - -
-
- ))} + ); + } + + if (integrationsWithLabels.length === 0) { + return ( +
+

+ No connections match your filter +

- )} + ); + } + + return ( +
+ {integrationsWithLabels.map((integration) => ( +
+
+ + {integration.label} + + {integration.name} + +
+
+ + + +
+
+ ))} +
+ ); + }; + + return ( +
+ {renderIntegrationsList()} {(showCreateDialog || editingIntegration) && ( ; integration?: string; }; @@ -23,21 +45,18 @@ const SYSTEM_ACTIONS: ActionType[] = [ label: "HTTP Request", description: "Make an HTTP request to any API", category: "System", - icon: Zap, }, { id: "Database Query", label: "Database Query", description: "Query your database", category: "System", - icon: Database, }, { id: "Condition", label: "Condition", description: "Branch based on a condition", category: "System", - icon: Settings, }, ]; @@ -65,16 +84,70 @@ type ActionGridProps = { isNewlyCreated?: boolean; }; -function ActionIcon({ action }: { action: ActionType }) { +function GroupIcon({ + group, +}: { + group: { category: string; actions: ActionType[] }; +}) { + // For plugin categories, use the integration icon from the first action + const firstAction = group.actions[0]; + if (firstAction?.integration) { + return ( + + ); + } + // For System category + if (group.category === "System") { + return ; + } + return ; +} + +function ActionIcon({ + action, + className, +}: { + action: ActionType; + className?: string; +}) { if (action.integration) { return ( - + ); } - if (action.icon) { - return ; + if (action.category === "System") { + return ; + } + return ; +} + +// Local storage keys +const HIDDEN_GROUPS_KEY = "workflow-action-grid-hidden-groups"; +const VIEW_MODE_KEY = "workflow-action-grid-view-mode"; + +type ViewMode = "list" | "grid"; + +function getInitialHiddenGroups(): Set { + if (typeof window === "undefined") return new Set(); + try { + const stored = localStorage.getItem(HIDDEN_GROUPS_KEY); + return stored ? new Set(JSON.parse(stored)) : new Set(); + } catch { + return new Set(); + } +} + +function getInitialViewMode(): ViewMode { + if (typeof window === "undefined") return "list"; + try { + const stored = localStorage.getItem(VIEW_MODE_KEY); + return stored === "grid" ? "grid" : "list"; + } catch { + return "list"; } - return ; } export function ActionGrid({ @@ -83,9 +156,49 @@ export function ActionGrid({ isNewlyCreated, }: ActionGridProps) { const [filter, setFilter] = useState(""); + const [collapsedGroups, setCollapsedGroups] = useState>( + new Set() + ); + const [hiddenGroups, setHiddenGroups] = useState>( + getInitialHiddenGroups + ); + const [showHidden, setShowHidden] = useState(false); + const [viewMode, setViewMode] = useState(getInitialViewMode); const actions = useAllActions(); const inputRef = useRef(null); + const toggleViewMode = () => { + const newMode = viewMode === "list" ? "grid" : "list"; + setViewMode(newMode); + localStorage.setItem(VIEW_MODE_KEY, newMode); + }; + + const toggleGroup = (category: string) => { + setCollapsedGroups((prev) => { + const next = new Set(prev); + if (next.has(category)) { + next.delete(category); + } else { + next.add(category); + } + return next; + }); + }; + + const toggleHideGroup = (category: string) => { + setHiddenGroups((prev) => { + const next = new Set(prev); + if (next.has(category)) { + next.delete(category); + } else { + next.add(category); + } + // Persist to localStorage + localStorage.setItem(HIDDEN_GROUPS_KEY, JSON.stringify([...next])); + return next; + }); + }; + useEffect(() => { if (isNewlyCreated && inputRef.current) { inputRef.current.focus(); @@ -101,49 +214,236 @@ export function ActionGrid({ ); }); + // Group actions by category + const groupedActions = useMemo(() => { + const groups: Record = {}; + + for (const action of filteredActions) { + const category = action.category; + if (!groups[category]) { + groups[category] = []; + } + groups[category].push(action); + } + + // Sort categories: System first, then alphabetically + const sortedCategories = Object.keys(groups).sort((a, b) => { + if (a === "System") return -1; + if (b === "System") return 1; + return a.localeCompare(b); + }); + + return sortedCategories.map((category) => ({ + category, + actions: groups[category], + })); + }, [filteredActions]); + + // Filter groups based on hidden state + const visibleGroups = useMemo(() => { + if (showHidden) return groupedActions; + return groupedActions.filter((g) => !hiddenGroups.has(g.category)); + }, [groupedActions, hiddenGroups, showHidden]); + + const hiddenCount = hiddenGroups.size; + return (
-
- - setFilter(e.target.value)} - placeholder="Search actions..." - ref={inputRef} - value={filter} - /> +
+
+ + setFilter(e.target.value)} + placeholder="Search actions..." + ref={inputRef} + value={filter} + /> +
+ + + + + + + {viewMode === "list" ? "Grid view" : "List view"} + + + + {hiddenCount > 0 && ( + + + + + + + {showHidden + ? "Hide hidden groups" + : `Show ${hiddenCount} hidden group${hiddenCount > 1 ? "s" : ""}`} + + + + )}
- {filteredActions.length === 0 ? ( + {filteredActions.length === 0 && (

No actions found

- ) : ( - filteredActions.map((action) => ( - - )) )} + {filteredActions.length > 0 && visibleGroups.length === 0 && ( +

+ All groups are hidden +

+ )} + + {/* Grid View */} + {viewMode === "grid" && visibleGroups.length > 0 && ( +
+ {filteredActions + .filter( + (action) => showHidden || !hiddenGroups.has(action.category) + ) + .map((action) => ( + + ))} +
+ )} + + {/* List View */} + {viewMode === "list" && + visibleGroups.length > 0 && + visibleGroups.map((group, groupIndex) => { + const isCollapsed = collapsedGroups.has(group.category); + const isHidden = hiddenGroups.has(group.category); + return ( +
+ {groupIndex > 0 &&
} +
+ + + + + + + toggleHideGroup(group.category)} + > + {isHidden ? ( + <> + + Show group + + ) : ( + <> + + Hide group + + )} + + + +
+ {!isCollapsed && + group.actions.map((action) => ( + + ))} +
+ ); + })}
); diff --git a/plugins/registry.ts b/plugins/registry.ts index 9e73c407..ef16bea6 100644 --- a/plugins/registry.ts +++ b/plugins/registry.ts @@ -372,6 +372,17 @@ export function getIntegrationLabels(): Record { return labels as Record; } +/** + * Get integration descriptions map + */ +export function getIntegrationDescriptions(): Record { + const descriptions: Record = {}; + for (const plugin of integrationRegistry.values()) { + descriptions[plugin.type] = plugin.description; + } + return descriptions as Record; +} + /** * Get sorted integration types for dropdowns */