mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-24 06:42:21 +08:00
fix(ui): refactor connection panel and fix search empty state (#12659)
* fix(ui): refactor connection panel and fix search empty state rendering Extract connection state management into useConnectionPanelState hook and search/list UI into ConnectionSearchList component. Fixes bug where blank list items rendered before the empty state when search returned no results. * fix(ui): prevent modal jump when clicking connection items in Chrome Chrome scrolls the nearest scrollable ancestor when focusing sr-only inputs inside labels. Adding relative + overflow-hidden to the label contains the scroll-into-view behavior within the label bounds.
This commit is contained in:
@ -7,6 +7,7 @@ export interface DeploymentConfigItem {
|
||||
connection_id: string;
|
||||
app_id: string;
|
||||
type?: string;
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
export interface DeploymentConfigListResponse {
|
||||
|
||||
@ -0,0 +1,95 @@
|
||||
import { memo, useMemo, useState } from "react";
|
||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { ConnectionItem } from "../types";
|
||||
import { CheckboxSelectItem } from "./radio-select-item";
|
||||
|
||||
interface ConnectionSearchListProps {
|
||||
connections: ConnectionItem[];
|
||||
selectedConnections: Set<string>;
|
||||
onToggleConnection: (id: string) => void;
|
||||
onSwitchToCreate: () => void;
|
||||
}
|
||||
|
||||
export const ConnectionSearchList = memo(function ConnectionSearchList({
|
||||
connections,
|
||||
selectedConnections,
|
||||
onToggleConnection,
|
||||
onSwitchToCreate,
|
||||
}: ConnectionSearchListProps) {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const filteredConnections = useMemo(() => {
|
||||
const sorted = [...connections].sort((a, b) => {
|
||||
if (a.isNew && !b.isNew) return -1;
|
||||
if (!a.isNew && b.isNew) return 1;
|
||||
return 0;
|
||||
});
|
||||
if (!searchQuery.trim()) return sorted;
|
||||
const q = searchQuery.toLowerCase();
|
||||
return sorted.filter(
|
||||
(c) => c.name.toLowerCase().includes(q) || c.id.toLowerCase().includes(q),
|
||||
);
|
||||
}, [connections, searchQuery]);
|
||||
|
||||
if (connections.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-3 py-12 text-center">
|
||||
<ForwardedIconComponent
|
||||
name="PlugZap"
|
||||
className="h-8 w-8 text-muted-foreground/50"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
No connections yet
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground/70">
|
||||
Create a connection to attach credentials to this flow.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSwitchToCreate}
|
||||
className="text-xs font-medium text-primary hover:underline"
|
||||
>
|
||||
Create your first connection
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-w-0">
|
||||
<Input
|
||||
icon="Search"
|
||||
placeholder="Search connections..."
|
||||
className="bg-muted"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{filteredConnections.length === 0 ? (
|
||||
<p className="py-6 text-center text-sm text-muted-foreground">
|
||||
No connections match “{searchQuery}”
|
||||
</p>
|
||||
) : (
|
||||
filteredConnections.map((conn) => (
|
||||
<CheckboxSelectItem
|
||||
key={conn.connectionId}
|
||||
value={conn.id}
|
||||
checked={selectedConnections.has(conn.id)}
|
||||
onChange={() => onToggleConnection(conn.id)}
|
||||
data-testid={`connection-item-${conn.id}`}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-medium leading-tight">
|
||||
{conn.name}
|
||||
</span>
|
||||
</div>
|
||||
</CheckboxSelectItem>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@ -31,7 +31,7 @@ export function CheckboxSelectItem({
|
||||
<label
|
||||
data-testid={testId}
|
||||
className={cn(
|
||||
"flex w-full cursor-pointer items-center gap-4 rounded-xl border bg-muted p-3 text-left transition-colors",
|
||||
"relative flex w-full cursor-pointer items-center gap-4 overflow-hidden rounded-xl border bg-muted p-3 text-left transition-colors",
|
||||
checked ? "border-primary" : "border-transparent hover:border-border",
|
||||
)}
|
||||
>
|
||||
@ -72,7 +72,7 @@ export function RadioSelectItem({
|
||||
<label
|
||||
data-testid={testId}
|
||||
className={cn(
|
||||
"flex w-full cursor-pointer items-center gap-4 rounded-xl border bg-muted p-3 text-left transition-colors",
|
||||
"relative flex w-full cursor-pointer items-center gap-4 overflow-hidden rounded-xl border bg-muted p-3 text-left transition-colors",
|
||||
selected ? "border-primary" : "border-transparent hover:border-border",
|
||||
className,
|
||||
)}
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import { memo, useMemo, useState } from "react";
|
||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import { memo } from "react";
|
||||
import InputComponent from "@/components/core/parameterRenderComponent/components/inputComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/utils/utils";
|
||||
import type { ConnectionItem, EnvVarEntry } from "../types";
|
||||
import { CheckboxSelectItem } from "./radio-select-item";
|
||||
import { ConnectionSearchList } from "./connection-search-list";
|
||||
|
||||
export type ConnectionTab = "available" | "create";
|
||||
|
||||
@ -48,21 +47,6 @@ export const ConnectionPanel = memo(function ConnectionPanel({
|
||||
onCreateConnection: () => void;
|
||||
isDuplicateName?: boolean;
|
||||
}) {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const filteredConnections = useMemo(() => {
|
||||
// Sort newly created connections to the top
|
||||
const sorted = [...connections].sort((a, b) => {
|
||||
if (a.isNew && !b.isNew) return -1;
|
||||
if (!a.isNew && b.isNew) return 1;
|
||||
return 0;
|
||||
});
|
||||
if (!searchQuery.trim()) return sorted;
|
||||
const q = searchQuery.toLowerCase();
|
||||
return sorted.filter(
|
||||
(c) => c.name.toLowerCase().includes(q) || c.id.toLowerCase().includes(q),
|
||||
);
|
||||
}, [connections, searchQuery]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-b border-border p-4 text-sm text-muted-foreground">
|
||||
@ -93,65 +77,18 @@ export const ConnectionPanel = memo(function ConnectionPanel({
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="mt-4 flex-1 overflow-x-hidden overflow-y-auto">
|
||||
<div
|
||||
className="mt-4 flex-1 overflow-x-hidden overflow-y-auto"
|
||||
key={connectionTab}
|
||||
>
|
||||
{connectionTab === "available" ? (
|
||||
<div className="min-w-0 space-y-3">
|
||||
{connections.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center gap-3 py-12 text-center">
|
||||
<ForwardedIconComponent
|
||||
name="PlugZap"
|
||||
className="h-8 w-8 text-muted-foreground/50"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
No connections yet
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground/70">
|
||||
Create a connection to attach credentials to this flow.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onTabChange("create")}
|
||||
className="text-xs font-medium text-primary hover:underline"
|
||||
>
|
||||
Create your first connection
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="min-w-0">
|
||||
<Input
|
||||
icon="Search"
|
||||
placeholder="Search connections..."
|
||||
className="bg-muted"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{filteredConnections.length === 0 ? (
|
||||
<p className="py-6 text-center text-sm text-muted-foreground">
|
||||
No connections match “{searchQuery}”
|
||||
</p>
|
||||
) : (
|
||||
filteredConnections.map((conn) => (
|
||||
<CheckboxSelectItem
|
||||
key={conn.id}
|
||||
value={conn.id}
|
||||
checked={selectedConnections.has(conn.id)}
|
||||
onChange={() => onToggleConnection(conn.id)}
|
||||
data-testid={`connection-item-${conn.id}`}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-medium leading-tight">
|
||||
{conn.name}
|
||||
</span>
|
||||
</div>
|
||||
</CheckboxSelectItem>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<ConnectionSearchList
|
||||
connections={connections}
|
||||
selectedConnections={selectedConnections}
|
||||
onToggleConnection={onToggleConnection}
|
||||
onSwitchToCreate={() => onTabChange("create")}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
@ -8,8 +8,8 @@ import { usePostDetectEnvVars } from "@/controllers/API/queries/variables/use-po
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import { useFolderStore } from "@/stores/foldersStore";
|
||||
import { useDeploymentStepper } from "../contexts/deployment-stepper-context";
|
||||
import type { ConnectionItem, EnvVarEntry } from "../types";
|
||||
import type { ConnectionTab } from "./step-attach-flows-connection-panel";
|
||||
import { useConnectionPanelState } from "../hooks/use-connection-panel-state";
|
||||
import type { ConnectionItem } from "../types";
|
||||
import { ConnectionPanel } from "./step-attach-flows-connection-panel";
|
||||
import { FlowListPanel } from "./step-attach-flows-flow-list-panel";
|
||||
import { VersionPanel } from "./step-attach-flows-version-panel";
|
||||
@ -81,6 +81,7 @@ export default function StepAttachFlows() {
|
||||
const existingConnections: ConnectionItem[] = configsData.configs.map(
|
||||
(cfg) => ({
|
||||
id: cfg.app_id,
|
||||
connectionId: cfg.connection_id,
|
||||
name: cfg.app_id,
|
||||
variableCount: 0,
|
||||
isNew: false,
|
||||
@ -106,16 +107,53 @@ export default function StepAttachFlows() {
|
||||
versionTag: string;
|
||||
} | null>(null);
|
||||
const [rightPanel, setRightPanel] = useState<RightPanelView>("versions");
|
||||
const [connectionTab, setConnectionTab] =
|
||||
useState<ConnectionTab>("available");
|
||||
const [selectedConnections, setSelectedConnections] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const [newConnectionName, setNewConnectionName] = useState("");
|
||||
const [envVars, setEnvVars] = useState<EnvVarEntry[]>(() => [
|
||||
{ id: crypto.randomUUID(), key: "", value: "" },
|
||||
]);
|
||||
const [detectedVarCount, setDetectedVarCount] = useState(0);
|
||||
|
||||
const commitPendingAttachment = useCallback(() => {
|
||||
if (pendingAttachment) {
|
||||
onSelectVersion(
|
||||
pendingAttachment.flowId,
|
||||
pendingAttachment.versionId,
|
||||
pendingAttachment.versionTag,
|
||||
);
|
||||
setPendingAttachment(null);
|
||||
}
|
||||
}, [pendingAttachment, onSelectVersion]);
|
||||
|
||||
const resetPendingAttachment = useCallback(() => {
|
||||
setPendingAttachment(null);
|
||||
}, []);
|
||||
|
||||
const effectiveFlowId = selectedFlowId ?? flows[0]?.id ?? null;
|
||||
|
||||
const {
|
||||
connectionTab,
|
||||
setConnectionTab,
|
||||
selectedConnections,
|
||||
setSelectedConnections,
|
||||
newConnectionName,
|
||||
setNewConnectionName,
|
||||
envVars,
|
||||
detectedVarCount,
|
||||
isDuplicateConnectionName,
|
||||
handleAttachConnection,
|
||||
handleCreateConnection,
|
||||
handleSkipConnection,
|
||||
handleChangeFlow,
|
||||
handleAddEnvVar,
|
||||
handleEnvVarChange,
|
||||
handleEnvVarSelectGlobalVar,
|
||||
initConnectionsForFlow,
|
||||
updateDetectedEnvVars,
|
||||
} = useConnectionPanelState({
|
||||
connections,
|
||||
setConnections,
|
||||
effectiveFlowId,
|
||||
attachedConnectionByFlow,
|
||||
onAttachConnection,
|
||||
commitPendingAttachment,
|
||||
resetPendingAttachment,
|
||||
setRightPanel,
|
||||
});
|
||||
|
||||
const { mutateAsync: detectEnvVars } = usePostDetectEnvVars();
|
||||
const { data: globalVariables } = useGetGlobalVariables();
|
||||
@ -123,6 +161,8 @@ export default function StepAttachFlows() {
|
||||
|
||||
// When a flow+version are pre-selected from outside (e.g., canvas deploy button),
|
||||
// auto-advance to the connections panel and detect env vars for the pre-selected version.
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: intentionally run only on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => {
|
||||
const preSelected = initialFlowId
|
||||
? selectedVersionByFlow.get(initialFlowId)
|
||||
@ -137,17 +177,7 @@ export default function StepAttachFlows() {
|
||||
flow_version_ids: [preSelected.versionId],
|
||||
});
|
||||
const detected = result.variables ?? [];
|
||||
if (detected.length > 0) {
|
||||
setDetectedVarCount(detected.length);
|
||||
setEnvVars(
|
||||
detected.map((variableName) => ({
|
||||
id: crypto.randomUUID(),
|
||||
key: variableName,
|
||||
value: variableName,
|
||||
globalVar: true,
|
||||
})),
|
||||
);
|
||||
}
|
||||
updateDetectedEnvVars(detected);
|
||||
} catch {
|
||||
setErrorData({
|
||||
title: "Could not auto-detect environment variables",
|
||||
@ -156,17 +186,8 @@ export default function StepAttachFlows() {
|
||||
}
|
||||
};
|
||||
detect();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const isDuplicateConnectionName = useMemo(() => {
|
||||
const trimmed = newConnectionName.trim().toLowerCase();
|
||||
if (!trimmed) return false;
|
||||
return connections.some((c) => c.name.trim().toLowerCase() === trimmed);
|
||||
}, [newConnectionName, connections]);
|
||||
|
||||
const effectiveFlowId = selectedFlowId ?? flows[0]?.id ?? null;
|
||||
|
||||
const { data: versionResponse, isLoading: isLoadingVersions } =
|
||||
useGetFlowVersions(
|
||||
{ flowId: effectiveFlowId ?? "" },
|
||||
@ -187,13 +208,7 @@ export default function StepAttachFlows() {
|
||||
versionTag: version?.version_tag ?? "",
|
||||
});
|
||||
setRightPanel("connections");
|
||||
setSelectedConnections(
|
||||
new Set(attachedConnectionByFlow.get(effectiveFlowId) ?? []),
|
||||
);
|
||||
// Default to "create" tab when there are no existing connections
|
||||
if (connections.length === 0) {
|
||||
setConnectionTab("create");
|
||||
}
|
||||
initConnectionsForFlow(effectiveFlowId);
|
||||
|
||||
// Auto-detect global variable references via the backend detection endpoint
|
||||
try {
|
||||
@ -201,23 +216,9 @@ export default function StepAttachFlows() {
|
||||
flow_version_ids: [versionId],
|
||||
});
|
||||
const detected = result.variables ?? [];
|
||||
if (detected.length > 0) {
|
||||
setDetectedVarCount(detected.length);
|
||||
setEnvVars(
|
||||
detected.map((variableName) => ({
|
||||
id: crypto.randomUUID(),
|
||||
key: variableName,
|
||||
value: variableName,
|
||||
globalVar: true,
|
||||
})),
|
||||
);
|
||||
} else {
|
||||
setDetectedVarCount(0);
|
||||
setEnvVars([{ id: crypto.randomUUID(), key: "", value: "" }]);
|
||||
}
|
||||
updateDetectedEnvVars(detected);
|
||||
} catch {
|
||||
setDetectedVarCount(0);
|
||||
setEnvVars([{ id: crypto.randomUUID(), key: "", value: "" }]);
|
||||
updateDetectedEnvVars([]);
|
||||
setErrorData({
|
||||
title: "Could not auto-detect environment variables",
|
||||
list: ["Add them manually in the connection form."],
|
||||
@ -227,98 +228,13 @@ export default function StepAttachFlows() {
|
||||
[
|
||||
effectiveFlowId,
|
||||
versions,
|
||||
connections,
|
||||
detectEnvVars,
|
||||
attachedConnectionByFlow,
|
||||
setErrorData,
|
||||
initConnectionsForFlow,
|
||||
updateDetectedEnvVars,
|
||||
],
|
||||
);
|
||||
|
||||
const commitPendingAttachment = useCallback(() => {
|
||||
if (pendingAttachment) {
|
||||
onSelectVersion(
|
||||
pendingAttachment.flowId,
|
||||
pendingAttachment.versionId,
|
||||
pendingAttachment.versionTag,
|
||||
);
|
||||
setPendingAttachment(null);
|
||||
}
|
||||
}, [pendingAttachment, onSelectVersion]);
|
||||
|
||||
const handleAttachConnection = useCallback(() => {
|
||||
if (!effectiveFlowId) return;
|
||||
if (connectionTab === "available" && selectedConnections.size > 0) {
|
||||
commitPendingAttachment();
|
||||
onAttachConnection((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(effectiveFlowId, Array.from(selectedConnections));
|
||||
return next;
|
||||
});
|
||||
setRightPanel("versions");
|
||||
setSelectedConnections(new Set());
|
||||
}
|
||||
}, [
|
||||
effectiveFlowId,
|
||||
connectionTab,
|
||||
selectedConnections,
|
||||
onAttachConnection,
|
||||
commitPendingAttachment,
|
||||
]);
|
||||
|
||||
const handleCreateConnection = useCallback(() => {
|
||||
const filteredVars = envVars.filter((v) => v.key.trim());
|
||||
const environmentVariables: Record<string, string> = {};
|
||||
const globalVarKeys = new Set<string>();
|
||||
for (const v of filteredVars) {
|
||||
const key = v.key.trim();
|
||||
environmentVariables[key] = v.value;
|
||||
if (v.globalVar) {
|
||||
globalVarKeys.add(key);
|
||||
}
|
||||
}
|
||||
const sanitizedId = newConnectionName
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "_")
|
||||
.replace(/[^a-z0-9_]/g, "");
|
||||
const newConn = {
|
||||
id: sanitizedId,
|
||||
name: newConnectionName.trim(),
|
||||
variableCount: filteredVars.length,
|
||||
isNew: true,
|
||||
environmentVariables,
|
||||
globalVarKeys,
|
||||
};
|
||||
setConnections((prev) => [...prev, newConn]);
|
||||
setSelectedConnections(
|
||||
(prev) => new Set([...Array.from(prev), newConn.id]),
|
||||
);
|
||||
setConnectionTab("available");
|
||||
setNewConnectionName("");
|
||||
setEnvVars([{ id: crypto.randomUUID(), key: "", value: "" }]);
|
||||
}, [envVars, newConnectionName, setConnections]);
|
||||
|
||||
const handleSkipConnection = useCallback(() => {
|
||||
commitPendingAttachment();
|
||||
if (effectiveFlowId) {
|
||||
onAttachConnection((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.delete(effectiveFlowId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setRightPanel("versions");
|
||||
setSelectedConnections(new Set());
|
||||
}, [effectiveFlowId, onAttachConnection, commitPendingAttachment]);
|
||||
|
||||
const handleChangeFlow = useCallback(() => {
|
||||
setPendingAttachment(null);
|
||||
setRightPanel("versions");
|
||||
setSelectedConnections(new Set());
|
||||
setDetectedVarCount(0);
|
||||
setEnvVars([{ id: crypto.randomUUID(), key: "", value: "" }]);
|
||||
}, []);
|
||||
|
||||
const handleDetachFlow = useCallback(
|
||||
(flowId: string) => {
|
||||
handleRemoveAttachedFlow(flowId);
|
||||
@ -333,53 +249,13 @@ export default function StepAttachFlows() {
|
||||
[handleRemoveAttachedFlow, setToolNameByFlow],
|
||||
);
|
||||
|
||||
const handleSelectFlow = useCallback((flowId: string) => {
|
||||
setSelectedFlowId(flowId);
|
||||
setRightPanel("versions");
|
||||
setSelectedConnections(new Set());
|
||||
setDetectedVarCount(0);
|
||||
setEnvVars([{ id: crypto.randomUUID(), key: "", value: "" }]);
|
||||
}, []);
|
||||
|
||||
const handleAddEnvVar = useCallback(() => {
|
||||
setEnvVars((prev) => [
|
||||
...prev,
|
||||
{ id: crypto.randomUUID(), key: "", value: "" },
|
||||
]);
|
||||
}, []);
|
||||
|
||||
const handleEnvVarChange = useCallback(
|
||||
(id: string, field: "key" | "value", val: string) => {
|
||||
setEnvVars((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === id ? { ...item, [field]: val, globalVar: false } : item,
|
||||
),
|
||||
);
|
||||
const handleSelectFlow = useCallback(
|
||||
(flowId: string) => {
|
||||
setSelectedFlowId(flowId);
|
||||
setRightPanel("versions");
|
||||
setSelectedConnections(new Set());
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleEnvVarSelectGlobalVar = useCallback(
|
||||
(id: string, selected: string) => {
|
||||
setEnvVars((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === id
|
||||
? {
|
||||
...item,
|
||||
key:
|
||||
selected !== "" &&
|
||||
(item.key.trim() === "" ||
|
||||
(item.globalVar && item.key === item.value))
|
||||
? selected
|
||||
: item.key,
|
||||
value: selected,
|
||||
globalVar: selected !== "",
|
||||
}
|
||||
: item,
|
||||
),
|
||||
);
|
||||
},
|
||||
[],
|
||||
[setSelectedConnections],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -0,0 +1,223 @@
|
||||
import {
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import type { ConnectionTab } from "../components/step-attach-flows-connection-panel";
|
||||
import type { ConnectionItem, EnvVarEntry } from "../types";
|
||||
|
||||
interface UseConnectionPanelStateParams {
|
||||
connections: ConnectionItem[];
|
||||
setConnections: Dispatch<SetStateAction<ConnectionItem[]>>;
|
||||
effectiveFlowId: string | null;
|
||||
attachedConnectionByFlow: Map<string, string[]>;
|
||||
onAttachConnection: Dispatch<SetStateAction<Map<string, string[]>>>;
|
||||
commitPendingAttachment: () => void;
|
||||
resetPendingAttachment: () => void;
|
||||
setRightPanel: (panel: "versions" | "connections") => void;
|
||||
}
|
||||
|
||||
export function useConnectionPanelState({
|
||||
connections,
|
||||
setConnections,
|
||||
effectiveFlowId,
|
||||
attachedConnectionByFlow,
|
||||
onAttachConnection,
|
||||
commitPendingAttachment,
|
||||
resetPendingAttachment,
|
||||
setRightPanel,
|
||||
}: UseConnectionPanelStateParams) {
|
||||
const [connectionTab, setConnectionTab] =
|
||||
useState<ConnectionTab>("available");
|
||||
const [selectedConnections, setSelectedConnections] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const [newConnectionName, setNewConnectionName] = useState("");
|
||||
const [envVars, setEnvVars] = useState<EnvVarEntry[]>(() => [
|
||||
{ id: crypto.randomUUID(), key: "", value: "" },
|
||||
]);
|
||||
const [detectedVarCount, setDetectedVarCount] = useState(0);
|
||||
|
||||
const isDuplicateConnectionName = useMemo(() => {
|
||||
const trimmed = newConnectionName.trim().toLowerCase();
|
||||
if (!trimmed) return false;
|
||||
return connections.some((c) => c.name.trim().toLowerCase() === trimmed);
|
||||
}, [newConnectionName, connections]);
|
||||
|
||||
const handleAttachConnection = useCallback(() => {
|
||||
if (!effectiveFlowId) return;
|
||||
if (connectionTab === "available" && selectedConnections.size > 0) {
|
||||
commitPendingAttachment();
|
||||
onAttachConnection((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(effectiveFlowId, Array.from(selectedConnections));
|
||||
return next;
|
||||
});
|
||||
setRightPanel("versions");
|
||||
setSelectedConnections(new Set());
|
||||
}
|
||||
}, [
|
||||
effectiveFlowId,
|
||||
connectionTab,
|
||||
selectedConnections,
|
||||
onAttachConnection,
|
||||
commitPendingAttachment,
|
||||
setRightPanel,
|
||||
]);
|
||||
|
||||
const handleCreateConnection = useCallback(() => {
|
||||
const filteredVars = envVars.filter((v) => v.key.trim());
|
||||
const environmentVariables: Record<string, string> = {};
|
||||
const globalVarKeys = new Set<string>();
|
||||
for (const v of filteredVars) {
|
||||
const key = v.key.trim();
|
||||
environmentVariables[key] = v.value;
|
||||
if (v.globalVar) {
|
||||
globalVarKeys.add(key);
|
||||
}
|
||||
}
|
||||
const sanitizedId = newConnectionName
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "_")
|
||||
.replace(/[^a-z0-9_]/g, "");
|
||||
const newConn: ConnectionItem = {
|
||||
id: sanitizedId,
|
||||
connectionId: sanitizedId,
|
||||
name: newConnectionName.trim(),
|
||||
variableCount: filteredVars.length,
|
||||
isNew: true,
|
||||
environmentVariables,
|
||||
globalVarKeys,
|
||||
};
|
||||
setConnections((prev) => [...prev, newConn]);
|
||||
setSelectedConnections(
|
||||
(prev) => new Set([...Array.from(prev), newConn.id]),
|
||||
);
|
||||
setConnectionTab("available");
|
||||
setNewConnectionName("");
|
||||
setEnvVars([{ id: crypto.randomUUID(), key: "", value: "" }]);
|
||||
}, [envVars, newConnectionName, setConnections]);
|
||||
|
||||
const handleSkipConnection = useCallback(() => {
|
||||
commitPendingAttachment();
|
||||
if (effectiveFlowId) {
|
||||
onAttachConnection((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.delete(effectiveFlowId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setRightPanel("versions");
|
||||
setSelectedConnections(new Set());
|
||||
}, [
|
||||
effectiveFlowId,
|
||||
onAttachConnection,
|
||||
commitPendingAttachment,
|
||||
setRightPanel,
|
||||
]);
|
||||
|
||||
const handleChangeFlow = useCallback(() => {
|
||||
resetPendingAttachment();
|
||||
setRightPanel("versions");
|
||||
setSelectedConnections(new Set());
|
||||
setDetectedVarCount(0);
|
||||
setEnvVars([{ id: crypto.randomUUID(), key: "", value: "" }]);
|
||||
}, [resetPendingAttachment, setRightPanel]);
|
||||
|
||||
const handleAddEnvVar = useCallback(() => {
|
||||
setEnvVars((prev) => [
|
||||
...prev,
|
||||
{ id: crypto.randomUUID(), key: "", value: "" },
|
||||
]);
|
||||
}, []);
|
||||
|
||||
const handleEnvVarChange = useCallback(
|
||||
(id: string, field: "key" | "value", val: string) => {
|
||||
setEnvVars((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === id ? { ...item, [field]: val, globalVar: false } : item,
|
||||
),
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleEnvVarSelectGlobalVar = useCallback(
|
||||
(id: string, selected: string) => {
|
||||
setEnvVars((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === id
|
||||
? {
|
||||
...item,
|
||||
key:
|
||||
selected !== "" &&
|
||||
(item.key.trim() === "" ||
|
||||
(item.globalVar && item.key === item.value))
|
||||
? selected
|
||||
: item.key,
|
||||
value: selected,
|
||||
globalVar: selected !== "",
|
||||
}
|
||||
: item,
|
||||
),
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const initConnectionsForFlow = useCallback(
|
||||
(flowId: string) => {
|
||||
setSelectedConnections(
|
||||
new Set(attachedConnectionByFlow.get(flowId) ?? []),
|
||||
);
|
||||
if (connections.length === 0) {
|
||||
setConnectionTab("create");
|
||||
}
|
||||
},
|
||||
[attachedConnectionByFlow, connections.length],
|
||||
);
|
||||
|
||||
const updateDetectedEnvVars = useCallback(
|
||||
(vars: Array<{ key: string; global_variable_name?: string | null }>) => {
|
||||
if (vars.length > 0) {
|
||||
setDetectedVarCount(vars.length);
|
||||
setEnvVars(
|
||||
vars.map((v) => ({
|
||||
id: crypto.randomUUID(),
|
||||
key: v.key,
|
||||
value: v.global_variable_name ?? "",
|
||||
globalVar: Boolean(v.global_variable_name),
|
||||
})),
|
||||
);
|
||||
} else {
|
||||
setDetectedVarCount(0);
|
||||
setEnvVars([{ id: crypto.randomUUID(), key: "", value: "" }]);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
connectionTab,
|
||||
setConnectionTab,
|
||||
selectedConnections,
|
||||
setSelectedConnections,
|
||||
newConnectionName,
|
||||
setNewConnectionName,
|
||||
envVars,
|
||||
detectedVarCount,
|
||||
isDuplicateConnectionName,
|
||||
handleAttachConnection,
|
||||
handleCreateConnection,
|
||||
handleSkipConnection,
|
||||
handleChangeFlow,
|
||||
handleAddEnvVar,
|
||||
handleEnvVarChange,
|
||||
handleEnvVarSelectGlobalVar,
|
||||
initConnectionsForFlow,
|
||||
updateDetectedEnvVars,
|
||||
};
|
||||
}
|
||||
@ -9,7 +9,9 @@ export interface EnvVarEntry {
|
||||
|
||||
export interface ConnectionItem {
|
||||
id: string;
|
||||
connectionId: string;
|
||||
name: string;
|
||||
environment?: string;
|
||||
variableCount: number;
|
||||
isNew: boolean;
|
||||
environmentVariables: Record<string, string>;
|
||||
|
||||
Reference in New Issue
Block a user