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
1 change: 1 addition & 0 deletions react-compiler.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const REACT_COMPILER_ENABLED_DIRS = [
"src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/ZIndexEditor.tsx",
"src/components/Editor/IOEditor/IOZIndexEditor.tsx",
"src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/DynamicDataDropdown.tsx",
"src/components/shared/ReactFlow/FlowCanvas/Multiselect",
"src/components/shared/HighlightText.tsx",
"src/components/shared/AnnouncementBanners.tsx",

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BlockStack } from "@/components/ui/layout";
import { Text } from "@/components/ui/typography";
import { createStringList, truncate } from "@/utils/string";

import { getFlexNodeDisplayName } from "../FlexNode/utils";
import { isFlexNode, type NodesAndEdges } from "../types";
import { thisCannotBeUndone } from "./shared";

Expand Down Expand Up @@ -131,11 +132,8 @@ export function getDeleteConfirmationDetails(deletedElements: NodesAndEdges) {
function getNodeIdsForDisplay(nodes: Node[]) {
return nodes.map((node) => {
if (isFlexNode(node)) {
const textContent =
node.data.properties.title ||
truncate(node.data.properties.content, 12, { breakWords: false });

return `'${textContent.length ? textContent : "untitled"}' (Sticky Note)`;
const textContent = getFlexNodeDisplayName(node.data);
return `'${textContent}' (Sticky Note)`;
}

return node.id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { type MouseEvent, useEffect, useState } from "react";

import { BlockStack } from "@/components/ui/layout";
import { useIsMultiSelect } from "@/hooks/useIsMultiSelect";
import { cn } from "@/lib/utils";
import { useContextPanel } from "@/providers/ContextPanelProvider";

Expand Down Expand Up @@ -44,6 +45,8 @@ const FlexNode = ({ data, id, selected }: FlexNodeProps) => {

const { updateFlexNode, updateProperties } = useFlexNodeUpdate(data);

const { isMultiSelect, isMultiSelectRef } = useIsMultiSelect();

const toggleLock = () => {
updateFlexNode({ locked: !locked });
};
Expand Down Expand Up @@ -120,7 +123,7 @@ const FlexNode = ({ data, id, selected }: FlexNodeProps) => {
};

useEffect(() => {
if (selected) {
if (selected && !isMultiSelect) {
setIsContextPanelFocus(true);
setContent(
<FlexNodeEditor
Expand All @@ -136,11 +139,11 @@ const FlexNode = ({ data, id, selected }: FlexNodeProps) => {
}

return () => {
if (selected) {
if (selected && !isMultiSelectRef.current) {
clearContent();
}
};
}, [selected]);
}, [selected, isMultiSelect]);

useEffect(() => {
if (isContextPanelFocus) {
Expand Down
12 changes: 12 additions & 0 deletions src/components/shared/ReactFlow/FlowCanvas/FlexNode/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { type Node } from "@xyflow/react";

import { truncate } from "@/utils/string";

import type { FlexNodeData } from "./types";

export const DEFAULT_STICKY_NOTE = {
Expand Down Expand Up @@ -30,3 +32,13 @@ export const createFlexNode = (
className: locked ? "pointer-events-auto!" : undefined,
} as Node;
};

export function getFlexNodeDisplayName(data: FlexNodeData): string {
if (data.properties.title) {
return data.properties.title;
}
if (data.properties.content) {
return truncate(data.properties.content, 12, { breakWords: false });
}
return "untitled";
}
74 changes: 47 additions & 27 deletions src/components/shared/ReactFlow/FlowCanvas/FlowCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ import {
computeDropPositionFromRefs,
createGhostEdge,
} from "./GhostNode/utils";
import SelectionToolbar from "./SelectionToolbar";
import { MultiSelectPanel } from "./Multiselect/MultiSelectPanel";
import SelectionToolbar from "./Multiselect/SelectionToolbar";
import { handleGroupNodes } from "./Subgraphs/create/handleGroupNodes";
import { NewSubgraphDialog } from "./Subgraphs/create/NewSubgraphDialog";
import { canGroupNodes } from "./Subgraphs/create/utils";
Expand Down Expand Up @@ -130,7 +131,7 @@ const FlowCanvasContent = ({
}: FlowCanvasProps) => {
const initialCanvasLoaded = useRef(false);

const { clearContent } = useContextPanel();
const { clearContent, setContent, setOpen } = useContextPanel();
const { data: currentUserDetails } = useUserDetails();

useSubgraphKeyboardNavigation();
Expand All @@ -148,6 +149,13 @@ const FlowCanvasContent = ({
useIOSelectionPersistence();

const store = useStoreApi();
const getSelectedNodes = () =>
store
.getState()
.nodes.filter(
(node) => node.selected && node.type && SELECTABLE_NODES.has(node.type),
);

const { edges: specEdges, onEdgesChange } =
useComponentSpecToEdges(currentSubgraphSpec);
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
Expand Down Expand Up @@ -321,13 +329,6 @@ const FlowCanvasContent = ({
(node) => node.selected && node.type && SELECTABLE_NODES.has(node.type),
);

const selectedEdges = edges.filter((edge) => edge.selected);

const selectedElements = {
nodes: selectedNodes,
edges: selectedEdges,
};

const canUpgrade = selectedNodes.some(
(node) => node.type && UPGRADEABLE_NODES.has(node.type),
);
Expand Down Expand Up @@ -680,11 +681,12 @@ const FlowCanvasContent = ({
};

const onRemoveNodes = async () => {
const nodes = getSelectedNodes();
const confirmed = await triggerConfirmation(
getDeleteConfirmationDetails({ nodes: selectedNodes, edges: [] }),
getDeleteConfirmationDetails({ nodes, edges: [] }),
);
if (confirmed) {
onElementsRemove(selectedElements);
onElementsRemove({ nodes, edges: [] });
}
};

Expand Down Expand Up @@ -754,7 +756,7 @@ const FlowCanvasContent = ({
updatedComponentSpec: updatedSubgraphSpec,
newNodes,
updatedNodes,
} = duplicateNodes(currentSubgraphSpec, selectedNodes, {
} = duplicateNodes(currentSubgraphSpec, getSelectedNodes(), {
selected: true,
author: currentUserDetails?.id,
});
Expand All @@ -779,7 +781,7 @@ const FlowCanvasContent = ({
const includedNodes: Node[] = [];
const excludedNodes: Node[] = [];

selectedNodes.forEach((node) => {
getSelectedNodes().forEach((node) => {
if (node.type && !UPGRADEABLE_NODES.has(node.type)) {
excludedNodes.push(node);
return;
Expand Down Expand Up @@ -907,12 +909,13 @@ const FlowCanvasContent = ({

const onCopy = () => {
// Copy selected nodes to clipboard
if (selectedNodes.length > 0) {
const selectedNodesJson = JSON.stringify(selectedNodes);
const nodes = getSelectedNodes();
if (nodes.length > 0) {
const selectedNodesJson = JSON.stringify(nodes);
navigator.clipboard.writeText(selectedNodesJson).catch((err) => {
console.error("Failed to copy nodes to clipboard:", err);
});
const message = `Copied ${selectedNodes.length} nodes to clipboard`;
const message = `Copied ${nodes.length} nodes to clipboard`;
notify(message, "success");
}
};
Expand Down Expand Up @@ -1036,13 +1039,14 @@ const FlowCanvasContent = ({
};

const onAutoLayout = () => {
const nodes = getSelectedNodes();
const connectedEdges = edges.filter(
(edge) =>
selectedNodes.some((node) => node.id === edge.source) ||
selectedNodes.some((node) => node.id === edge.target),
nodes.some((node) => node.id === edge.source) ||
nodes.some((node) => node.id === edge.target),
);

applyAutoLayout(selectedNodes, connectedEdges);
applyAutoLayout(nodes, connectedEdges);
};

useImperativeHandle(
Expand All @@ -1053,6 +1057,29 @@ const FlowCanvasContent = ({
[handleAutoLayout],
);

const multiSelectCallbacks = {
onDelete: !readOnly ? onRemoveNodes : undefined,
onDuplicate: !readOnly ? onDuplicateNodes : undefined,
onUpgrade: !readOnly && canUpgrade ? onUpgradeNodes : undefined,
onGroup: !readOnly && canGroup ? onGroupNodes : undefined,
onCopy: onCopy,
onAutoLayout: onAutoLayout,
};

Comment thread
camielvs marked this conversation as resolved.
useEffect(() => {
const nodes = getSelectedNodes();
if (nodes.length > 1) {
setContent(
<MultiSelectPanel
selectedNodes={nodes}
readOnly={!!readOnly}
{...multiSelectCallbacks}
/>,
);
setOpen(true);
}
}, [currentSubgraphSpec, readOnly, canUpgrade, canGroup]);

return (
<BlockStack fill>
<SubgraphBreadcrumbs />
Expand Down Expand Up @@ -1096,14 +1123,7 @@ const FlowCanvasContent = ({
align="end"
className="z-9999!"
>
<SelectionToolbar
onDelete={!readOnly ? onRemoveNodes : undefined}
onDuplicate={!readOnly ? onDuplicateNodes : undefined}
onCopy={!readOnly ? undefined : onCopy}
onUpgrade={!readOnly && canUpgrade ? onUpgradeNodes : undefined}
onGroup={!readOnly && canGroup ? onGroupNodes : undefined}
onAutoLayout={onAutoLayout}
/>
<SelectionToolbar {...multiSelectCallbacks} />
</NodeToolbar>
{children}
</ReactFlow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { BlockStack, InlineStack } from "@/components/ui/layout";
import { QuickTooltip } from "@/components/ui/tooltip";
import { Paragraph } from "@/components/ui/typography";
import { useEdgeSelectionHighlight } from "@/hooks/useEdgeSelectionHighlight";
import { useIsMultiSelect } from "@/hooks/useIsMultiSelect";
import { cn } from "@/lib/utils";
import { useComponentSpec } from "@/providers/ComponentSpecProvider";
import { useContextPanel } from "@/providers/ContextPanelProvider";
Expand Down Expand Up @@ -83,6 +84,8 @@ const IONode = ({ id, type, data, selected = false }: IONodeProps) => {

const readOnly = !!data.readOnly;

const { isMultiSelect, isMultiSelectRef } = useIsMultiSelect();

const isInSubgraph = isViewingSubgraph(currentSubgraphPath);

const handleType = isInput ? "source" : "target";
Expand Down Expand Up @@ -117,7 +120,7 @@ const IONode = ({ id, type, data, selected = false }: IONodeProps) => {
);

useEffect(() => {
if (selected) {
if (selected && !isMultiSelect) {
if (input && isInput) {
setContent(
<InputValueEditor
Expand Down Expand Up @@ -148,11 +151,11 @@ const IONode = ({ id, type, data, selected = false }: IONodeProps) => {
}

return () => {
if (selected) {
if (selected && !isMultiSelectRef.current) {
clearContent();
}
};
}, [input, output, selected, readOnly]);
}, [input, output, selected, isMultiSelect, readOnly]);

const connectedOutput = getOutputConnectedDetails(
currentGraphSpec,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Node } from "@xyflow/react";

import { InfoBox } from "@/components/shared/InfoBox";
import { Paragraph } from "@/components/ui/typography";

import { FlexNodeEditor } from "../FlexNode/FlexNodeEditor";
import { isFlexNodeData } from "../FlexNode/types";

export const FlexDetailPanel = ({
node,
readOnly,
}: {
node: Node;
readOnly: boolean;
}) => {
const data = node.data;
if (!isFlexNodeData(data)) {
return (
<InfoBox title="Unable to Load Sticky Note Details" variant="error">
<Paragraph size="sm" tone="subdued">
Invalid data for Sticky Note.
</Paragraph>
</InfoBox>
);
}

return <FlexNodeEditor flexNode={data} readOnly={readOnly} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Node } from "@xyflow/react";

import { InputValueEditor } from "@/components/Editor/IOEditor/InputValueEditor";
import { InfoBox } from "@/components/shared/InfoBox";
import { Paragraph } from "@/components/ui/typography";
import { useComponentSpec } from "@/providers/ComponentSpecProvider";
import { nodeIdToInputName } from "@/utils/nodes/nodeIdUtils";

export const InputDetailPanel = ({
node,
readOnly,
}: {
node: Node;
readOnly: boolean;
}) => {
const { currentSubgraphSpec } = useComponentSpec();
const name = nodeIdToInputName(node.id);
const input = currentSubgraphSpec.inputs?.find((i) => i.name === name);

if (!input) {
return (
<InfoBox title="Unable to Load Input Details" variant="error">
<Paragraph size="sm" tone="subdued">
{`Input "${name}" not found in pipeline spec.`}
</Paragraph>
</InfoBox>
);
}

return <InputValueEditor input={input} disabled={readOnly} />;
};
Loading
Loading