Repository: Cloud-Pipelines/pipeline-studio-app
Pull request #213: Move Argument Editor to Side-Sheet
Author: login: camielvs
Created: 2025-04-28T22:36:28Z | Merged: 2025-04-30T17:31:39Z

Closes https://github.com/Shopify/oasis-frontend/issues/50

Migrates the Arguments Editor into a pop-up side-sheet.
Also adds a button to copy component yaml to clipboard.

<img width="1512" alt="image" src="https://github.com/user-attachments/assets/2d657da6-5d10-439f-be4c-c9077b29f3ca" />


**Known issues**

- Not strictly a regression, but it is now more difficult to copy arguments between nodes, since only one editor can now be open at a time. This will be addressed separately at some point in the future.
- There is a bug where unsetting an argument resets it to default when reopening the editor. This will be addressed as part of https://github.com/Shopify/oasis-frontend/issues/83, since that will rework a lot of the argument input logic anyway.
- There are a few other tidbits (particularly UX) relating to editing arguments which will be address as part of https://github.com/Shopify/oasis-frontend/issues/83 instead.

This PR does not address https://github.com/Shopify/oasis-frontend/issues/83 or https://github.com/Shopify/oasis-frontend/issues/84


Diff:
diff --git a/src/components/deprecated/Sidebar/PipelineSubmitter/PipelineSubmitter.tsx b/src/components/deprecated/Sidebar/PipelineSubmitter/PipelineSubmitter.tsx
index 3e02a7bd2..3a9c449b2 100644
--- a/src/components/deprecated/Sidebar/PipelineSubmitter/PipelineSubmitter.tsx
+++ b/src/components/deprecated/Sidebar/PipelineSubmitter/PipelineSubmitter.tsx
@@ -9,10 +9,8 @@
 import { useEffect, useState } from "react";
 
 import OasisSubmitter from "@/components/shared/OasisSubmitter";
-import {
-  type ArgumentInput,
-  ArgumentsEditor,
-} from "@/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor";
+import { ArgumentsEditor } from "@/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor";
+import type { ArgumentInput } from "@/types/arguments";
 import type { ComponentSpec } from "@/utils/componentSpec";
 
 import { GoogleCloudSubmitter, KubeflowPipelinesSubmitter } from "./Submitters";
diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx
index c3797d0dc..ac9c53c4e 100644
--- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx
+++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx
@@ -18,28 +18,17 @@ import {
   TooltipTrigger,
 } from "@/components/ui/tooltip";
 import { cn } from "@/lib/utils";
-import type {
-  ArgumentType,
-  InputSpec,
-  TypeSpecType,
-} from "@/utils/componentSpec";
-
-export type ArgumentInput = {
-  key: string;
-  value: ArgumentType;
-  initialValue: ArgumentType;
-  inputSpec: InputSpec;
-  isRemoved?: boolean;
-};
+import type { ArgumentInput } from "@/types/arguments";
+import type { ArgumentType, TypeSpecType } from "@/utils/componentSpec";
 
 export const ArgumentInputField = ({
   argument,
-  setArgument,
   disabled = false,
+  setArgument,
 }: {
   argument: ArgumentInput;
-  setArgument: (value: ArgumentInput) => void;
   disabled?: boolean;
+  setArgument: (value: ArgumentInput) => void;
 }) => {
   const [inputValue, setInputValue] = useState(getInputValue(argument));
 
@@ -97,28 +86,33 @@ export const ArgumentInputField = ({
   }, [argument]);
 
   return (
-    <div className="flex w-full items-center gap-2 py-1">
-      <div
-        className={cn(
-          "w-[256px] min-w-[256px] flex flex-col",
-          argument.isRemoved ? "opacity-50" : "",
-        )}
-      >
-        <Label
-          htmlFor={argument.inputSpec.name}
-          className="text-sm break-words"
-        >
-          {argument.inputSpec.name.replace(/_/g, " ")}
-        </Label>
-        <span
-          className="text-xs text-gray-500 truncate"
-          title={typeSpecToString(argument.inputSpec.type)}
+    <div className="flex w-full items-center justify-between gap-2 py-1">
+      <div className="flex items-center gap-2 w-2/5">
+        <div
+          className={cn(
+            "bg-warning rounded-full h-1 w-1",
+            !canUndo && "invisible",
+          )}
+        />
+        <div
+          className={cn("flex flex-col", argument.isRemoved && "opacity-50")}
         >
-          ({typeSpecToString(argument.inputSpec.type)}
-          {!argument.inputSpec.optional ? "*" : ""})
-        </span>
+          <Label
+            htmlFor={argument.inputSpec.name}
+            className="text-sm break-words"
+          >
+            {argument.inputSpec.name.replace(/_/g, " ")}
+          </Label>
+          <span
+            className="text-xs text-gray-500 truncate"
+            title={typeSpecToString(argument.inputSpec.type)}
+          >
+            ({typeSpecToString(argument.inputSpec.type)}
+            {!argument.inputSpec.optional ? "*" : ""})
+          </span>
+        </div>
       </div>
-      <div className="flex flex-1 items-center relative">
+      <div className="relative w-48">
         <Input
           id={argument.inputSpec.name}
           value={inputValue}
@@ -130,10 +124,12 @@ export const ArgumentInputField = ({
           className={cn(
             "flex-1",
             canUndo && "pr-10",
-            !argument.inputSpec.optional && argument.isRemoved
-              ? "border-red-200"
-              : "",
-            argument.isRemoved ? "border-gray-100 text-gray-500" : "",
+            argument.isRemoved &&
+              !argument.inputSpec.optional &&
+              "border-red-200",
+            argument.isRemoved &&
+              argument.inputSpec.optional &&
+              "border-gray-100 text-gray-500",
           )}
           disabled={disabled}
         />
@@ -142,7 +138,7 @@ export const ArgumentInputField = ({
             <TooltipTrigger asChild>
               <Button
                 onClick={handleUndo}
-                className="absolute right-1 text-xs h-min w-min p-1"
+                className="absolute right-1 top-1/2 transform -translate-y-1/2"
                 disabled={disabled}
                 variant="ghost"
                 style={{
@@ -157,7 +153,7 @@ export const ArgumentInputField = ({
         )}
       </div>
 
-      <div className="flex gap-1 items-center">
+      <div className="flex gap-1 items-center w-1/5 justify-end">
         <Tooltip>
           <TooltipTrigger asChild>
             <Button
diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsEditor.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsEditor.tsx
index 1b9570a9b..9bbee0c51 100644
--- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsEditor.tsx
+++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsEditor.tsx
@@ -7,8 +7,9 @@
  */
 
 import { TooltipProvider } from "@/components/ui/tooltip";
+import type { ArgumentInput } from "@/types/arguments";
 
-import { type ArgumentInput, ArgumentInputField } from "./ArgumentInputField";
+import { ArgumentInputField } from "./ArgumentInputField";
 
 interface ArgumentsEditorProps {
   argumentData: ArgumentInput[];
@@ -29,17 +30,15 @@ export const ArgumentsEditor = ({
 
   return (
     <TooltipProvider>
-      <div className="h-auto w-[650px] flex flex-col gap-2 max-h-[60vh] overflow-y-auto pr-4">
-        {argumentData.map((argument) => {
-          return (
-            <ArgumentInputField
-              key={argument.key}
-              argument={argument}
-              setArgument={handleInputChange}
-              disabled={disabled}
-            />
-          );
-        })}
+      <div className="h-auto flex flex-col gap-2 max-h-[60vh] overflow-y-auto pr-4">
+        {argumentData.map((argument) => (
+          <ArgumentInputField
+            key={argument.key}
+            argument={argument}
+            setArgument={handleInputChange}
+            disabled={disabled}
+          />
+        ))}
       </div>
     </TooltipProvider>
   );
diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsEditorDialog.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsEditorDialog.tsx
index 22ccc8190..94e6c9dd7 100644
--- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsEditorDialog.tsx
+++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsEditorDialog.tsx
@@ -10,9 +10,9 @@ import { Copy, Trash } from "lucide-react";
 import { useState } from "react";
 
 import { DraggableDialog } from "@/components/shared/Dialogs";
+import type { ArgumentInput } from "@/types/arguments";
 import type { ArgumentType, TaskSpec } from "@/utils/componentSpec";
 
-import type { ArgumentInput } from "./ArgumentInputField";
 import { ArgumentsEditor } from "./ArgumentsEditor";
 
 interface ArgumentsEditorDialogProps {
diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsSection.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsSection.tsx
new file mode 100644
index 000000000..6dedda387
--- /dev/null
+++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsSection.tsx
@@ -0,0 +1,86 @@
+import { AlertCircle } from "lucide-react";
+import { useState } from "react";
+
+import { Button } from "@/components/ui/button";
+import type { ArgumentInput } from "@/types/arguments";
+import type { ArgumentType, TaskSpec } from "@/utils/componentSpec";
+
+import { ArgumentsEditor } from "../ArgumentsEditor";
+
+interface ArgumentsSectionProps {
+  taskSpec: TaskSpec;
+  setArguments: (args: Record<string, ArgumentType>) => void;
+  disabled?: boolean;
+}
+
+const ArgumentsSection = ({
+  taskSpec,
+  setArguments,
+  disabled = false,
+}: ArgumentsSectionProps) => {
+  const componentSpec = taskSpec.componentRef.spec;
+
+  const argumentInputs =
+    componentSpec?.inputs?.map((input) => {
+      const existingArgument = taskSpec.arguments?.[input.name];
+      const initialValue = existingArgument ?? input.default;
+
+      return {
+        key: input.name,
+        value: initialValue ?? "",
+        initialValue: initialValue ?? "",
+        inputSpec: input,
+        isRemoved: initialValue === undefined,
+      } as ArgumentInput;
+    }) ?? [];
+
+  const [currentArguments, setCurrentArguments] =
+    useState<ArgumentInput[]>(argumentInputs);
+
+  if (componentSpec === undefined) {
+    console.error(
+      "ArgumentsEditor called with missing taskSpec.componentRef.spec",
+      taskSpec,
+    );
+    return null;
+  }
+
+  const handleApply = () => {
+    const argumentValues = Object.fromEntries(
+      currentArguments
+        .filter(({ isRemoved }) => !isRemoved)
+        .map(({ key, value }) => [key, value]),
+    );
+
+    setArguments(argumentValues);
+  };
+
+  const dirty =
+    JSON.stringify(currentArguments) !== JSON.stringify(argumentInputs);
+
+  return (
+    <div className="flex-1 overflow-y-auto p-2">
+      <ArgumentsEditor
+        argumentData={currentArguments}
+        setArguments={setCurrentArguments}
+        disabled={disabled}
+      />
+      <div className="flex justify-end gap-4 p-4 items-center">
+        {dirty && (
+          <div className="text-sm text-warning flex gap-1 items-center">
+            <AlertCircle height={16} /> Unsaved changes
+          </div>
+        )}
+        <Button
+          onClick={handleApply}
+          disabled={disabled}
+          className="cursor-pointer"
+        >
+          Apply
+        </Button>
+      </div>
+    </div>
+  );
+};
+
+export default ArgumentsSection;
diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskConfigurationSheet/TaskConfigurationSheet.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskConfigurationSheet/TaskConfigurationSheet.tsx
new file mode 100644
index 000000000..33f03e661
--- /dev/null
+++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskConfigurationSheet/TaskConfigurationSheet.tsx
@@ -0,0 +1,118 @@
+import { Info } from "lucide-react";
+import { type ReactNode } from "react";
+
+import CondensedUrl from "@/components/shared/CondensedUrl";
+import { Button, type ButtonProps } from "@/components/ui/button";
+import {
+  Sheet,
+  SheetContent,
+  SheetDescription,
+  SheetFooter,
+  SheetHeader,
+  SheetTitle,
+  SheetTrigger,
+} from "@/components/ui/sheet";
+import type { ArgumentType, TaskSpec } from "@/utils/componentSpec";
+import { TOP_NAV_HEIGHT } from "@/utils/constants";
+
+import ArgumentsSection from "../ArgumentsEditor/ArgumentsSection";
+
+interface TaskConfigurationSheetProps {
+  trigger: ReactNode;
+  taskId: string;
+  taskSpec: TaskSpec;
+  actions?: ButtonProps[];
+  isOpen: boolean;
+  disabled?: boolean;
+  onOpenChange: (isOpen: boolean) => void;
+  setArguments: (args: Record<string, ArgumentType>) => void;
+  onDelete?: () => void;
+}
+
+const TaskConfigurationSheet = ({
+  trigger,
+  taskId,
+  taskSpec,
+  actions,
+  isOpen,
+  disabled = false,
+  onOpenChange,
+  setArguments,
+}: TaskConfigurationSheetProps) => {
+  const componentSpec = taskSpec.componentRef.spec;
+
+  if (componentSpec === undefined) {
+    console.error(
+      "ArgumentsEditor called with missing taskSpec.componentRef.spec",
+      taskSpec,
+    );
+    return null;
+  }
+
+  const sheetHeight = window.innerHeight - TOP_NAV_HEIGHT;
+
+  return (
+    <Sheet open={isOpen} onOpenChange={onOpenChange}>
+      <SheetTrigger asChild>{trigger}</SheetTrigger>
+      <SheetContent
+        side="right"
+        className={`!w-[512px] !max-w-[512px] top-[${TOP_NAV_HEIGHT}px] shadow-none`}
+        style={{
+          height: sheetHeight + "px",
+        }}
+        overlay={false}
+      >
+        <SheetHeader>
+          <SheetTitle>
+            <div className="flex gap-2">
+              <Info />
+              {componentSpec.name ?? "<component>"}
+            </div>
+          </SheetTitle>
+          <SheetDescription>id: {taskId}</SheetDescription>
+          {taskSpec.componentRef.url && (
+            <div className="flex gap-2 text-sidebar-primary text-sm">
+              Source:
+              <CondensedUrl url={taskSpec.componentRef.url} />
+            </div>
+          )}
+          <div className="mt-4 flex flex-col gap-2">
+            <div className="flex gap-2">
+              {actions &&
+                actions.map((action, index) => (
+                  <Button key={index} {...action} />
+                ))}
+            </div>
+          </div>
+        </SheetHeader>
+        <hr className="mx-4" />
+        <div className="flex flex-col overflow-y-auto px-4 gap-4">
+          <div>
+            <h2>Arguments</h2>
+            <p className="text-sm text-muted-foreground">
+              Configure the arguments for this task node.
+            </p>
+            <ArgumentsSection
+              taskSpec={taskSpec}
+              setArguments={setArguments}
+              disabled={disabled}
+            />
+          </div>
+        </div>
+        <hr className="mx-4" />
+        <SheetFooter>
+          <Button
+            onClick={() => onOpenChange(false)}
+            disabled={disabled}
+            variant="secondary"
+            className="w-fit cursor-pointer"
+          >
+            Close
+          </Button>
+        </SheetFooter>
+      </SheetContent>
+    </Sheet>
+  );
+};
+
+export default TaskConfigurationSheet;
diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskConfigurationSheet/index.ts b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskConfigurationSheet/index.ts
new file mode 100644
index 000000000..56c4c2a72
--- /dev/null
+++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskConfigurationSheet/index.ts
@@ -0,0 +1 @@
+export { default } from "./TaskConfigurationSheet";
diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNode.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNode.tsx
index 1ae9d7d03..4fd77d82c 100644
--- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNode.tsx
+++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNode.tsx
@@ -5,9 +5,10 @@
  * @author         Alexey Volkov <alexey.volkov+oss@ark-kun.com>
  * @copyright 2021 Alexey Volkov <alexey.volkov+oss@ark-kun.com>
  */
-
 import type { HandleType, NodeProps } from "@xyflow/react";
 import { Handle, Position } from "@xyflow/react";
+import yaml from "js-yaml";
+import { BookCopy, Copy, Trash } from "lucide-react";
 import {
   type CSSProperties,
   memo,
@@ -17,11 +18,17 @@ import {
 } from "react";
 
 import { useDynamicFontSize } from "@/hooks/useDynamicFontSize";
+import useToastNotification from "@/hooks/useToastNotification";
 import { cn } from "@/lib/utils";
 import type { TaskNodeCallbacks } from "@/types/taskNode";
-import type { InputSpec, OutputSpec, TaskSpec } from "@/utils/componentSpec";
-
-import ArgumentsEditorDialog from "./ArgumentsEditor";
+import type {
+  ArgumentType,
+  InputSpec,
+  OutputSpec,
+  TaskSpec,
+} from "@/utils/componentSpec";
+
+import TaskConfigurationSheet from "./TaskConfigurationSheet";
 import TaskDetailsSheet from "./TaskDetailsSheet";
 
 const inputHandlePosition = Position.Top;
@@ -139,13 +146,15 @@ function generateOutputHandles(outputSpecs: OutputSpec[]): ReactElement[] {
 }
 
 const ComponentTaskNode = ({ data, selected }: NodeProps) => {
-  const [isArgumentsEditorOpen, setIsArgumentsEditorOpen] = useState(false);
+  const [isComponentEditorOpen, setIsComponentEditorOpen] = useState(false);
   const [isTaskDetailsSheetOpen, setIsTaskDetailsSheetOpen] = useState(false);
   const nodeRef = useRef<HTMLDivElement>(null);
   const textRef = useRef<HTMLDivElement>(null);
 
   useDynamicFontSize(textRef);
 
+  const notify = useToastNotification();
+
   const typedData = data as ComponentTaskNodeProps;
   const taskSpec = typedData.taskSpec;
   const componentSpec = taskSpec.componentRef.spec;
@@ -199,10 +208,6 @@ const ComponentTaskNode = ({ data, selected }: NodeProps) => {
   const outputHandles = generateOutputHandles(componentSpec.outputs ?? []);
   const handleComponents = inputHandles.concat(outputHandles);
 
-  const closeArgumentsEditor = () => {
-    setIsArgumentsEditorOpen(false);
-  };
-
   const getBorderColor = () => {
     switch (runStatus) {
       case "SUCCEEDED":
@@ -242,102 +247,127 @@ const ComponentTaskNode = ({ data, selected }: NodeProps) => {
   };
 
   const handleClick = () => {
-    if (!isArgumentsEditorOpen && !runStatus) {
-      setIsArgumentsEditorOpen(true);
+    if (!isComponentEditorOpen && !runStatus) {
+      setIsComponentEditorOpen(true);
     }
     if (!isTaskDetailsSheetOpen && runStatus) {
       setIsTaskDetailsSheetOpen(true);
     }
   };
 
-  const handleDelete = () => {
-    typedData.onDelete();
+  const handleSetArguments = (args: Record<string, ArgumentType>) => {
+    typedData.setArguments(args);
+    notify("Arguments updated", "success");
   };
 
-  const handleTaskDetailsSheetClose = () => {
-    setIsTaskDetailsSheetOpen(false);
+  const handleDeleteTaskNode = () => {
+    typedData.onDelete();
   };
 
-  const handleCopy = () => {
+  const handleDuplicateTaskNode = () => {
     typedData.onDuplicate();
-    setIsArgumentsEditorOpen(false);
+    setIsComponentEditorOpen(false);
+  };
+
+  const handleCopyYaml = () => {
+    const code = yaml.dump(componentSpec?.implementation, {
+      lineWidth: 80,
+      noRefs: true,
+      indent: 2,
+    });
+
+    navigator.clipboard.writeText(code).then(
+      () => notify("Implementation copied to clipboard", "success"),
+      (err) => notify("Failed to copy Implementation: " + err, "error"),
+    );
+  };
+
+  const handleTaskDetailsSheetClose = () => {
+    setIsTaskDetailsSheetOpen(false);
   };
 
   return (
     <>
-      <div
-        className={cn(
-          "border rounded-md shadow-sm transition-all duration-200",
-          getBorderColor(),
-          getBgColor(),
-          selected ? "border-sky-500" : " hover:border-slate-400",
-        )}
-        style={{ width: `${NODE_WIDTH_IN_PX}px` }}
-        ref={nodeRef}
-        onClick={handleClick}
-      >
-        <div className="p-3 flex items-center justify-between">
+      <TaskConfigurationSheet
+        taskId={typedData.taskId}
+        taskSpec={taskSpec}
+        isOpen={isComponentEditorOpen}
+        onOpenChange={setIsComponentEditorOpen}
+        actions={[
+          {
+            children: (
+              <div className="flex items-center gap-2">
+                <BookCopy />
+                Copy yaml
+              </div>
+            ),
+            variant: "secondary",
+            className: "cursor-pointer",
+            onClick: handleCopyYaml,
+          },
+          {
+            children: (
+              <div className="flex items-center gap-2">
+                <Copy />
+                Duplicate
+              </div>
+            ),
+            variant: "secondary",
+            className: "cursor-pointer",
+            onClick: handleDuplicateTaskNode,
+          },
+          {
+            children: (
+              <div className="flex items-center gap-2">
+                <Trash />
+                Delete
+              </div>
+            ),
+            variant: "destructive",
+            className: "cursor-pointer",
+            onClick: handleDeleteTaskNode,
+          },
+        ]}
+        setArguments={handleSetArguments}
+        disabled={!!runStatus}
+        trigger={
           <div
-            className="font-medium text-gray-800 whitespace-nowrap w-full text-center"
-            title={title}
-            ref={textRef}
-          >
-            {label}
-          </div>
-          <div className="flex items-center gap-2">
-            {runStatus && (
-              <TaskDetailsSheet
-                isOpen={isTaskDetailsSheetOpen}
-                taskSpec={taskSpec}
-                taskId={typedData.taskId}
-                runStatus={runStatus}
-                onClose={handleTaskDetailsSheetClose}
-              />
+            className={cn(
+              "border rounded-md shadow-sm transition-all duration-200",
+              getBorderColor(),
+              getBgColor(),
+              selected ? "border-sky-500" : " hover:border-slate-400",
             )}
+            style={{ width: `${NODE_WIDTH_IN_PX}px` }}
+            ref={nodeRef}
+            onClick={handleClick}
+          >
+            <div className="p-3 flex items-center justify-between">
+              <div
+                className="font-medium text-gray-800 whitespace-nowrap w-full text-center"
+                title={title}
+                ref={textRef}
+              >
+                {label}
+              </div>
+              <div className="flex items-center gap-2">
+                {runStatus && (
+                  <TaskDetailsSheet
+                    isOpen={isTaskDetailsSheetOpen}
+                    taskSpec={taskSpec}
+                    taskId={typedData.taskId}
+                    runStatus={runStatus}
+                    onClose={handleTaskDetailsSheetClose}
+                  />
+                )}
+              </div>
+              {handleComponents}
+            </div>
           </div>
-        </div>
-      </div>
-      {handleComponents}
-      {isArgumentsEditorOpen && nodeRef.current && (
-        <ArgumentsEditorDialog
-          taskSpec={taskSpec}
-          closeEditor={closeArgumentsEditor}
-          setArguments={typedData.setArguments}
-          disabled={!!runStatus}
-          initialPosition={getDialogPosition(nodeRef.current, 650)}
-          handleDelete={handleDelete}
-          handleCopy={handleCopy}
-        />
-      )}
+        }
+      />
     </>
   );
 };
 
 export default memo(ComponentTaskNode);
-
-const SMALL_SCREEEN_BREAKPOINT = 720;
-const DIALOG_SPACING = 8;
-const getDialogPosition = (node: HTMLElement, dialogWidth: number) => {
-  // On large screens position the dialog to the right of the node
-  // On small screens position the dialog below the node & center it
-  // Assumed 650px width for the dialog & 720px small screen breakpoint
-
-  const windowWidth = window.innerWidth;
-
-  const positionBelow =
-    windowWidth <= SMALL_SCREEEN_BREAKPOINT ||
-    node.getBoundingClientRect().right + DIALOG_SPACING >
-      windowWidth - dialogWidth;
-
-  return {
-    x: positionBelow
-      ? node.getBoundingClientRect().left +
-        node.getBoundingClientRect().width / 2 -
-        dialogWidth / 2
-      : node.getBoundingClientRect().right + DIALOG_SPACING,
-
-    y: positionBelow
-      ? node.getBoundingClientRect().bottom + DIALOG_SPACING
-      : node.getBoundingClientRect().top,
-  };
-};
diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx
index 901269924..9ab6d21d7 100644
--- a/src/components/ui/sheet.tsx
+++ b/src/components/ui/sheet.tsx
@@ -46,13 +46,16 @@ function SheetContent({
   className,
   children,
   side = "right",
+  overlay = true,
   ...props
 }: React.ComponentProps<typeof SheetPrimitive.Content> & {
   side?: "top" | "right" | "bottom" | "left";
+  overlay?: boolean;
 }) {
   return (
     <SheetPortal>
-      <SheetOverlay />
+      {/* The optional overlay is a custom modification to the standard Shadcn Sheet component */}
+      {overlay && <SheetOverlay />}
       <SheetPrimitive.Content
         data-slot="sheet-content"
         className={cn(
diff --git a/src/styles/global.css b/src/styles/global.css
index 11c30c5b4..da0e227e0 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -35,6 +35,8 @@ code {
   --accent-foreground: oklch(0.205 0 0);
   --destructive: oklch(0.577 0.245 27.325);
   --destructive-foreground: oklch(0.577 0.245 27.325);
+  --warning: oklch(0.85 0.2 50);
+  --warning-foreground: oklch(0.145 0 0);
   --border: oklch(0.922 0 0);
   --input: oklch(0.922 0 0);
   --ring: oklch(0.708 0 0);
@@ -71,6 +73,8 @@ code {
   --accent-foreground: oklch(0.985 0 0);
   --destructive: oklch(0.396 0.141 25.723);
   --destructive-foreground: oklch(0.637 0.237 25.331);
+  --warning: oklch(0.85 0.2 50);
+  --warning-foreground: oklch(0.145 0 0);
   --border: oklch(0.269 0 0);
   --input: oklch(0.269 0 0);
   --ring: oklch(0.439 0 0);
@@ -106,6 +110,8 @@ code {
   --color-accent-foreground: var(--accent-foreground);
   --color-destructive: var(--destructive);
   --color-destructive-foreground: var(--destructive-foreground);
+  --color-warning: var(--warning);
+  --color-warning-foreground: var(--warning-foreground);
   --color-border: var(--border);
   --color-input: var(--input);
   --color-ring: var(--ring);
diff --git a/src/types/arguments.ts b/src/types/arguments.ts
new file mode 100644
index 000000000..8c5533d44
--- /dev/null
+++ b/src/types/arguments.ts
@@ -0,0 +1,13 @@
+import type { ArgumentType, InputSpec } from "@/utils/componentSpec";
+
+/**
+ * Argument input to the Argument Editor.
+ */
+
+export type ArgumentInput = {
+  key: string;
+  value: ArgumentType;
+  initialValue: ArgumentType;
+  inputSpec: InputSpec;
+  isRemoved?: boolean;
+};
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 7414faab2..b507e3e06 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -40,3 +40,6 @@ export const DB_NAME = "components";
 export const PIPELINE_RUNS_STORE_NAME = "pipeline_runs";
 
 export const USER_COMPONENTS_LIST_NAME = "user_components";
+
+export const TOP_NAV_HEIGHT = 56; // px
+export const FOOTER_HEIGHT = 30; // px


Reviews:
user: login: camielvs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T00:21:10Z
user: login: camielvs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T00:23:23Z
user: login: camielvs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T00:24:05Z
user: login: camielvs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T00:29:13Z
user: login: Mbeaulne
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T15:36:31Z
user: login: Mbeaulne
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T15:37:05Z
user: login: Mbeaulne
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T15:38:02Z
user: login: Mbeaulne
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T15:39:23Z
user: login: camielvs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T16:18:41Z
user: login: Mbeaulne
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-30T17:22:18Z

Review comments:
diff_hunk: @@ -0,0 +1,113 @@
+import { Info } from "lucide-react";
+import { type ReactNode } from "react";
+
+import CondensedUrl from "@/components/shared/CondensedUrl";
+import { Button, type ButtonProps } from "@/components/ui/button";
+import {
+  Sheet,
+  SheetContent,
+  SheetDescription,
+  SheetFooter,
+  SheetHeader,
+  SheetTitle,
+  SheetTrigger,
+} from "@/components/ui/sheet";
+import type { ArgumentType, TaskSpec } from "@/utils/componentSpec";
+
+import ArgumentsSection from "../ArgumentsEditor/ArgumentsSection";
+
+interface TaskConfigurationSheetProps {
+  trigger: ReactNode;
+  taskId: string;
+  taskSpec: TaskSpec;
+  actions?: ButtonProps[];
+  isOpen: boolean;
+  disabled?: boolean;
+  onOpenChange: (isOpen: boolean) => void;
+  setArguments: (args: Record<string, ArgumentType>) => void;
+  onDelete?: () => void;
+}
+
+const TaskConfigurationSheet = ({
+  trigger,
+  taskId,
+  taskSpec,
+  actions,
+  isOpen,
+  disabled = false,
+  onOpenChange,
+  setArguments,
+}: TaskConfigurationSheetProps) => {
+  const componentSpec = taskSpec.componentRef.spec;
+
+  if (componentSpec === undefined) {
+    console.error(
+      "ArgumentsEditor called with missing taskSpec.componentRef.spec",
+      taskSpec,
+    );
+    return null;
+  }
+
+  return (
+    <Sheet open={isOpen} onOpenChange={onOpenChange}>
+      <SheetTrigger asChild>{trigger}</SheetTrigger>
+      <SheetContent
+        side="right"
+        className="w-fit !max-w-fit top-[56px] shadow-none"
+        style={{ height: window.innerHeight - 56 - 30 + "px" }}
+        overlay={false}
+      >
+        <SheetHeader>
+          <SheetTitle>
+            <div className="flex gap-2">
+              <Info />
+              {componentSpec.name ?? "<component>"}
+            </div>
+          </SheetTitle>
+          <SheetDescription>{`id: ${taskId}`}</SheetDescription>
+          {taskSpec.componentRef.url && (
+            <div className="flex gap-2 text-sidebar-primary text-sm">
+              Source:
+              <CondensedUrl url={taskSpec.componentRef.url} />
+            </div>
+          )}
+          <div className="mt-4 flex flex-col gap-2">
+            <div className="flex gap-2">
+              {actions &&
+                actions.map((action, index) => (
+                  <Button key={index} {...action} />
+                ))}
+            </div>
+          </div>
+        </SheetHeader>
+        <hr className="mx-4" />
+        <div className="flex flex-col overflow-y-auto px-4 gap-4">
path: src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskConfigurationSheet/TaskConfigurationSheet.tsx
user: login: camielvs
body: i don't love the design, and particularly the sidebar/scroll area. suggestions welcome
created_at: 2025-04-30T00:21:10Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,113 @@
+import { Info } from "lucide-react";
+import { type ReactNode } from "react";
+
+import CondensedUrl from "@/components/shared/CondensedUrl";
+import { Button, type ButtonProps } from "@/components/ui/button";
+import {
+  Sheet,
+  SheetContent,
+  SheetDescription,
+  SheetFooter,
+  SheetHeader,
+  SheetTitle,
+  SheetTrigger,
+} from "@/components/ui/sheet";
+import type { ArgumentType, TaskSpec } from "@/utils/componentSpec";
+
+import ArgumentsSection from "../ArgumentsEditor/ArgumentsSection";
+
+interface TaskConfigurationSheetProps {
+  trigger: ReactNode;
+  taskId: string;
+  taskSpec: TaskSpec;
+  actions?: ButtonProps[];
+  isOpen: boolean;
+  disabled?: boolean;
+  onOpenChange: (isOpen: boolean) => void;
+  setArguments: (args: Record<string, ArgumentType>) => void;
+  onDelete?: () => void;
+}
+
+const TaskConfigurationSheet = ({
+  trigger,
+  taskId,
+  taskSpec,
+  actions,
+  isOpen,
+  disabled = false,
+  onOpenChange,
+  setArguments,
+}: TaskConfigurationSheetProps) => {
+  const componentSpec = taskSpec.componentRef.spec;
+
+  if (componentSpec === undefined) {
+    console.error(
+      "ArgumentsEditor called with missing taskSpec.componentRef.spec",
+      taskSpec,
+    );
+    return null;
+  }
+
+  return (
+    <Sheet open={isOpen} onOpenChange={onOpenChange}>
+      <SheetTrigger asChild>{trigger}</SheetTrigger>
+      <SheetContent
+        side="right"
+        className="w-fit !max-w-fit top-[56px] shadow-none"
+        style={{ height: window.innerHeight - 56 - 30 + "px" }}
+        overlay={false}
path: src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskConfigurationSheet/TaskConfigurationSheet.tsx
user: login: camielvs
body: The side-sheet is simply a sheet positioned on the right without an overlay. To achieve this I modified the shadcn-generated Sheet component to make overlays optional.

edit: The TaskDetailsSheet is also a side-sheet but has been built slightly differently. As a follow-up I might look into this to see if they can be aligned/standardized.
created_at: 2025-04-30T00:23:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,113 @@
+import { Info } from "lucide-react";
+import { type ReactNode } from "react";
+
+import CondensedUrl from "@/components/shared/CondensedUrl";
+import { Button, type ButtonProps } from "@/components/ui/button";
+import {
+  Sheet,
+  SheetContent,
+  SheetDescription,
+  SheetFooter,
+  SheetHeader,
+  SheetTitle,
+  SheetTrigger,
+} from "@/components/ui/sheet";
+import type { ArgumentType, TaskSpec } from "@/utils/componentSpec";
+
+import ArgumentsSection from "../ArgumentsEditor/ArgumentsSection";
+
+interface TaskConfigurationSheetProps {
+  trigger: ReactNode;
+  taskId: string;
+  taskSpec: TaskSpec;
+  actions?: ButtonProps[];
+  isOpen: boolean;
+  disabled?: boolean;
+  onOpenChange: (isOpen: boolean) => void;
+  setArguments: (args: Record<string, ArgumentType>) => void;
+  onDelete?: () => void;
+}
+
+const TaskConfigurationSheet = ({
+  trigger,
+  taskId,
+  taskSpec,
+  actions,
+  isOpen,
+  disabled = false,
+  onOpenChange,
+  setArguments,
+}: TaskConfigurationSheetProps) => {
+  const componentSpec = taskSpec.componentRef.spec;
+
+  if (componentSpec === undefined) {
+    console.error(
+      "ArgumentsEditor called with missing taskSpec.componentRef.spec",
+      taskSpec,
+    );
+    return null;
+  }
+
+  return (
+    <Sheet open={isOpen} onOpenChange={onOpenChange}>
+      <SheetTrigger asChild>{trigger}</SheetTrigger>
+      <SheetContent
+        side="right"
+        className="w-fit !max-w-fit top-[56px] shadow-none"
+        style={{ height: window.innerHeight - 56 - 30 + "px" }}
+        overlay={false}
path: src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskConfigurationSheet/TaskConfigurationSheet.tsx
user: login: camielvs
body: I also don't love the manual calculation of height and position (accounting for top nav and bottom footer here)
created_at: 2025-04-30T00:24:05Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,86 @@
+import { AlertCircle } from "lucide-react";
+import { useState } from "react";
+
+import { Button } from "@/components/ui/button";
+import type { ArgumentInput } from "@/types/arguments";
+import type { ArgumentType, TaskSpec } from "@/utils/componentSpec";
+
+import { ArgumentsEditor } from "../ArgumentsEditor";
+
+interface ArgumentsSectionProps {
+  taskSpec: TaskSpec;
+  setArguments: (args: Record<string, ArgumentType>) => void;
+  disabled?: boolean;
+}
+
+const ArgumentsSection = ({
+  taskSpec,
+  setArguments,
+  disabled = false,
+}: ArgumentsSectionProps) => {
+  const componentSpec = taskSpec.componentRef.spec;
+
+  const argumentInputs =
+    componentSpec?.inputs?.map((input) => {
+      const existingArgument = taskSpec.arguments?.[input.name];
+      const initialValue = existingArgument ?? input.default;
+
+      return {
+        key: input.name,
+        value: initialValue ?? "",
+        initialValue: initialValue ?? "",
+        inputSpec: input,
+        isRemoved: initialValue === undefined,
+      } as ArgumentInput;
+    }) ?? [];
+
+  const [currentArguments, setCurrentArguments] =
+    useState<ArgumentInput[]>(argumentInputs);
+
+  if (componentSpec === undefined) {
+    console.error(
+      "ArgumentsEditor called with missing taskSpec.componentRef.spec",
+      taskSpec,
+    );
+    return null;
+  }
+
+  const handleApply = () => {
+    const argumentValues = Object.fromEntries(
+      currentArguments
+        .filter(({ isRemoved }) => !isRemoved)
+        .map(({ key, value }) => [key, value]),
+    );
+
+    setArguments(argumentValues);
+  };
+
+  const dirty =
+    JSON.stringify(currentArguments) !== JSON.stringify(argumentInputs);
+
+  return (
+    <div className="flex-1 overflow-y-auto p-2">
+      <ArgumentsEditor
+        argumentData={currentArguments}
+        setArguments={setCurrentArguments}
+        disabled={disabled}
+      />
+      <div className="flex justify-end gap-4 p-4 items-center">
+        {dirty && (
+          <div className="text-sm text-orange-300 flex gap-1 items-center">
+            <AlertCircle height={16} /> Unsaved changes
+          </div>
+        )}
+        <Button
+          onClick={handleApply}
+          disabled={disabled}
+          className="cursor-pointer"
+        >
+          Apply
+        </Button>
path: src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsSection.tsx
user: login: camielvs
body: I don't particularly like that I have to scroll down to the apply button. But the button will be removed soon as per https://github.com/Shopify/oasis-frontend/issues/83 so I don't think it's worth engineering a temporary alternative
created_at: 2025-04-30T00:29:13Z
author_association: COLLABORATOR
diff_hunk: @@ -242,102 +247,127 @@ const ComponentTaskNode = ({ data, selected }: NodeProps) => {
   };
 
   const handleClick = () => {
-    if (!isArgumentsEditorOpen && !runStatus) {
-      setIsArgumentsEditorOpen(true);
+    if (!isComponentEditorOpen && !runStatus) {
+      setIsComponentEditorOpen(true);
     }
     if (!isTaskDetailsSheetOpen && runStatus) {
       setIsTaskDetailsSheetOpen(true);
     }
   };
 
-  const handleDelete = () => {
-    typedData.onDelete();
+  const handleSetArguments = (args: Record<string, ArgumentType>) => {
+    typedData.setArguments(args);
+    notify("Arguments updated", "success");
   };
 
-  const handleTaskDetailsSheetClose = () => {
-    setIsTaskDetailsSheetOpen(false);
+  const handleDeleteTaskNode = () => {
+    typedData.onDelete();
   };
 
-  const handleCopy = () => {
+  const handleDuplicateTaskNode = () => {
     typedData.onDuplicate();
-    setIsArgumentsEditorOpen(false);
+    setIsComponentEditorOpen(false);
+  };
+
+  const handleCopyYaml = () => {
+    const code = yaml.dump(componentSpec?.implementation, {
+      lineWidth: 80,
+      noRefs: true,
+      indent: 2,
+    });
+
+    navigator.clipboard.writeText(code).then(
+      () => notify("Implementation copied to clipboard", "success"),
+      (err) => notify("Failed to copy Implementation: " + err, "error"),
+    );
+  };
+
+  const handleTaskDetailsSheetClose = () => {
+    setIsTaskDetailsSheetOpen(false);
   };
 
   return (
     <>
-      <div
-        className={cn(
-          "border rounded-md shadow-sm transition-all duration-200",
-          getBorderColor(),
-          getBgColor(),
-          selected ? "border-sky-500" : " hover:border-slate-400",
-        )}
-        style={{ width: `${NODE_WIDTH_IN_PX}px` }}
-        ref={nodeRef}
-        onClick={handleClick}
-      >
-        <div className="p-3 flex items-center justify-between">
+      <TaskConfigurationSheet
+        taskId={typedData.taskId}
+        taskSpec={taskSpec}
+        isOpen={isComponentEditorOpen}
+        onOpenChange={setIsComponentEditorOpen}
+        actions={[
+          {
+            children: (
+              <div className="flex items-center gap-2">
+                <BookCopy />
+                Copy yaml
+              </div>
+            ),
+            variant: "secondary",
+            className: "cursor-pointer",
+            onClick: handleCopyYaml,
+          },
+          {
+            children: (
+              <div className="flex items-center gap-2">
+                <Copy />
+                Duplicate
+              </div>
+            ),
+            variant: "secondary",
+            className: "cursor-pointer",
+            onClick: handleDuplicateTaskNode,
+          },
+          {
+            children: (
+              <div className="flex items-center gap-2">
+                <Trash />
+                Delete
+              </div>
+            ),
+            variant: "destructive",
+            className: "cursor-pointer",
+            onClick: handleDeleteTaskNode,
+          },
+        ]}
+        setArguments={handleSetArguments}
+        disabled={!!runStatus}
+        trigger={
           <div
-            className="font-medium text-gray-800 whitespace-nowrap w-full text-center"
-            title={title}
-            ref={textRef}
-          >
-            {label}
-          </div>
-          <div className="flex items-center gap-2">
-            {runStatus && (
-              <TaskDetailsSheet
-                isOpen={isTaskDetailsSheetOpen}
-                taskSpec={taskSpec}
-                taskId={typedData.taskId}
-                runStatus={runStatus}
-                onClose={handleTaskDetailsSheetClose}
-              />
+            className={cn(
+              "border rounded-md shadow-sm transition-all duration-200",
+              getBorderColor(),
+              getBgColor(),
+              selected ? "border-sky-500" : " hover:border-slate-400",
             )}
+            style={{ width: `${NODE_WIDTH_IN_PX}px` }}
path: src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNode.tsx
user: login: Mbeaulne
body: can we make this tailwind? `w-[${NODE_WIDTH_IN_PX}px]`
created_at: 2025-04-30T15:36:31Z
author_association: COLLABORATOR
diff_hunk: @@ -242,102 +247,127 @@ const ComponentTaskNode = ({ data, selected }: NodeProps) => {
   };
 
   const handleClick = () => {
-    if (!isArgumentsEditorOpen && !runStatus) {
-      setIsArgumentsEditorOpen(true);
+    if (!isComponentEditorOpen && !runStatus) {
+      setIsComponentEditorOpen(true);
     }
     if (!isTaskDetailsSheetOpen && runStatus) {
       setIsTaskDetailsSheetOpen(true);
     }
   };
 
-  const handleDelete = () => {
-    typedData.onDelete();
+  const handleSetArguments = (args: Record<string, ArgumentType>) => {
+    typedData.setArguments(args);
+    notify("Arguments updated", "success");
   };
 
-  const handleTaskDetailsSheetClose = () => {
-    setIsTaskDetailsSheetOpen(false);
+  const handleDeleteTaskNode = () => {
+    typedData.onDelete();
   };
 
-  const handleCopy = () => {
+  const handleDuplicateTaskNode = () => {
     typedData.onDuplicate();
-    setIsArgumentsEditorOpen(false);
+    setIsComponentEditorOpen(false);
+  };
+
+  const handleCopyYaml = () => {
+    const code = yaml.dump(componentSpec?.implementation, {
+      lineWidth: 80,
+      noRefs: true,
+      indent: 2,
+    });
+
+    navigator.clipboard.writeText(code).then(
+      () => notify("Implementation copied to clipboard", "success"),
+      (err) => notify("Failed to copy Implementation: " + err, "error"),
+    );
path: src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNode.tsx
user: login: Mbeaulne
body: Dunno why, but I really like this.
created_at: 2025-04-30T15:37:05Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,113 @@
+import { Info } from "lucide-react";
+import { type ReactNode } from "react";
+
+import CondensedUrl from "@/components/shared/CondensedUrl";
+import { Button, type ButtonProps } from "@/components/ui/button";
+import {
+  Sheet,
+  SheetContent,
+  SheetDescription,
+  SheetFooter,
+  SheetHeader,
+  SheetTitle,
+  SheetTrigger,
+} from "@/components/ui/sheet";
+import type { ArgumentType, TaskSpec } from "@/utils/componentSpec";
+
+import ArgumentsSection from "../ArgumentsEditor/ArgumentsSection";
+
+interface TaskConfigurationSheetProps {
+  trigger: ReactNode;
+  taskId: string;
+  taskSpec: TaskSpec;
+  actions?: ButtonProps[];
+  isOpen: boolean;
+  disabled?: boolean;
+  onOpenChange: (isOpen: boolean) => void;
+  setArguments: (args: Record<string, ArgumentType>) => void;
+  onDelete?: () => void;
+}
+
+const TaskConfigurationSheet = ({
+  trigger,
+  taskId,
+  taskSpec,
+  actions,
+  isOpen,
+  disabled = false,
+  onOpenChange,
+  setArguments,
+}: TaskConfigurationSheetProps) => {
+  const componentSpec = taskSpec.componentRef.spec;
+
+  if (componentSpec === undefined) {
+    console.error(
+      "ArgumentsEditor called with missing taskSpec.componentRef.spec",
+      taskSpec,
+    );
+    return null;
+  }
+
+  return (
+    <Sheet open={isOpen} onOpenChange={onOpenChange}>
+      <SheetTrigger asChild>{trigger}</SheetTrigger>
+      <SheetContent
+        side="right"
+        className="w-fit !max-w-fit top-[56px] shadow-none"
+        style={{ height: window.innerHeight - 56 - 30 + "px" }}
+        overlay={false}
+      >
+        <SheetHeader>
+          <SheetTitle>
+            <div className="flex gap-2">
+              <Info />
+              {componentSpec.name ?? "<component>"}
+            </div>
+          </SheetTitle>
+          <SheetDescription>{`id: ${taskId}`}</SheetDescription>
path: src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskConfigurationSheet/TaskConfigurationSheet.tsx
user: login: Mbeaulne
body: NIT:
`<SheetDescription>id: {taskId}</SheetDescription>`
created_at: 2025-04-30T15:38:02Z
author_association: COLLABORATOR
diff_hunk: @@ -46,13 +46,15 @@ function SheetContent({
   className,
   children,
   side = "right",
+  overlay = true,
   ...props
 }: React.ComponentProps<typeof SheetPrimitive.Content> & {
   side?: "top" | "right" | "bottom" | "left";
+  overlay?: boolean;
 }) {
   return (
     <SheetPortal>
-      <SheetOverlay />
+      {overlay && <SheetOverlay />}
path: src/components/ui/sheet.tsx
user: login: Mbeaulne
body: can you comment here saying this is custom? I know sometimes installing other components will ask you to update existing components and I would not want a regression
created_at: 2025-04-30T15:39:23Z
author_association: COLLABORATOR
diff_hunk: @@ -242,102 +247,127 @@ const ComponentTaskNode = ({ data, selected }: NodeProps) => {
   };
 
   const handleClick = () => {
-    if (!isArgumentsEditorOpen && !runStatus) {
-      setIsArgumentsEditorOpen(true);
+    if (!isComponentEditorOpen && !runStatus) {
+      setIsComponentEditorOpen(true);
     }
     if (!isTaskDetailsSheetOpen && runStatus) {
       setIsTaskDetailsSheetOpen(true);
     }
   };
 
-  const handleDelete = () => {
-    typedData.onDelete();
+  const handleSetArguments = (args: Record<string, ArgumentType>) => {
+    typedData.setArguments(args);
+    notify("Arguments updated", "success");
   };
 
-  const handleTaskDetailsSheetClose = () => {
-    setIsTaskDetailsSheetOpen(false);
+  const handleDeleteTaskNode = () => {
+    typedData.onDelete();
   };
 
-  const handleCopy = () => {
+  const handleDuplicateTaskNode = () => {
     typedData.onDuplicate();
-    setIsArgumentsEditorOpen(false);
+    setIsComponentEditorOpen(false);
+  };
+
+  const handleCopyYaml = () => {
+    const code = yaml.dump(componentSpec?.implementation, {
+      lineWidth: 80,
+      noRefs: true,
+      indent: 2,
+    });
+
+    navigator.clipboard.writeText(code).then(
+      () => notify("Implementation copied to clipboard", "success"),
+      (err) => notify("Failed to copy Implementation: " + err, "error"),
+    );
+  };
+
+  const handleTaskDetailsSheetClose = () => {
+    setIsTaskDetailsSheetOpen(false);
   };
 
   return (
     <>
-      <div
-        className={cn(
-          "border rounded-md shadow-sm transition-all duration-200",
-          getBorderColor(),
-          getBgColor(),
-          selected ? "border-sky-500" : " hover:border-slate-400",
-        )}
-        style={{ width: `${NODE_WIDTH_IN_PX}px` }}
-        ref={nodeRef}
-        onClick={handleClick}
-      >
-        <div className="p-3 flex items-center justify-between">
+      <TaskConfigurationSheet
+        taskId={typedData.taskId}
+        taskSpec={taskSpec}
+        isOpen={isComponentEditorOpen}
+        onOpenChange={setIsComponentEditorOpen}
+        actions={[
+          {
+            children: (
+              <div className="flex items-center gap-2">
+                <BookCopy />
+                Copy yaml
+              </div>
+            ),
+            variant: "secondary",
+            className: "cursor-pointer",
+            onClick: handleCopyYaml,
+          },
+          {
+            children: (
+              <div className="flex items-center gap-2">
+                <Copy />
+                Duplicate
+              </div>
+            ),
+            variant: "secondary",
+            className: "cursor-pointer",
+            onClick: handleDuplicateTaskNode,
+          },
+          {
+            children: (
+              <div className="flex items-center gap-2">
+                <Trash />
+                Delete
+              </div>
+            ),
+            variant: "destructive",
+            className: "cursor-pointer",
+            onClick: handleDeleteTaskNode,
+          },
+        ]}
+        setArguments={handleSetArguments}
+        disabled={!!runStatus}
+        trigger={
           <div
-            className="font-medium text-gray-800 whitespace-nowrap w-full text-center"
-            title={title}
-            ref={textRef}
-          >
-            {label}
-          </div>
-          <div className="flex items-center gap-2">
-            {runStatus && (
-              <TaskDetailsSheet
-                isOpen={isTaskDetailsSheetOpen}
-                taskSpec={taskSpec}
-                taskId={typedData.taskId}
-                runStatus={runStatus}
-                onClose={handleTaskDetailsSheetClose}
-              />
+            className={cn(
+              "border rounded-md shadow-sm transition-all duration-200",
+              getBorderColor(),
+              getBgColor(),
+              selected ? "border-sky-500" : " hover:border-slate-400",
             )}
+            style={{ width: `${NODE_WIDTH_IN_PX}px` }}
path: src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNode.tsx
user: login: camielvs
body: Are you sure this works? I've always been under the impression you can't use computed parameters directly within Tailwind strings. I'm also not seeing the css applied when trying it
created_at: 2025-04-30T16:18:41Z
author_association: COLLABORATOR

PR comments:
user: login: Mbeaulne
created_at: 2025-04-30T15:29:05Z
body: NIT: some components have different size sheets. Maybe we should make it more consistent?  either in this PR or make a fast follow P2 ticket
<img width="1687" alt="Screenshot 2025-04-30 at 11 28 04 AM" src="https://github.com/user-attachments/assets/0a046f59-7f7d-4d57-8819-a5b495d75eec" />
<img width="1685" alt="Screenshot 2025-04-30 at 11 28 16 AM" src="https://github.com/user-attachments/assets/c03a62f7-dd42-40d4-877a-e32ffe747d04" />

author_association: COLLABORATOR
user: login: Mbeaulne
created_at: 2025-04-30T15:31:40Z
body: Nit or fast follow up:
<img width="489" alt="Screenshot 2025-04-30 at 11 30 33 AM" src="https://github.com/user-attachments/assets/07aff5e8-3d39-4f59-8d8a-908852bce8dc" />

The view repo should have a new window icon next to it. Does shadCN buttons or anything automatically support this?

author_association: COLLABORATOR
user: login: Mbeaulne
created_at: 2025-04-30T15:35:11Z
body: There is something strange happening where your nodes are still selected after duplicating them
https://github.com/user-attachments/assets/8bc2e774-9d52-4182-9ffd-cec626a50079


author_association: COLLABORATOR
user: login: Mbeaulne
created_at: 2025-04-30T15:42:10Z
body: This is a great (for lack of a better word) starting point for this side sheet! I've left a few comments, but its more or less good to go
author_association: COLLABORATOR
user: login: camielvs
created_at: 2025-04-30T16:35:21Z
body: > There is something strange happening where your nodes are still selected after duplicating them https://github.com/user-attachments/assets/8bc2e774-9d52-4182-9ffd-cec626a50079

Woah, strange. That is an issue with the selection state, not this PR - I will investigate separately.

Edit: I checked if this issue appears on the dupe nodes branch (https://github.com/Cloud-Pipelines/pipeline-studio-app/pull/169) and it does not. So whatever issue with the selection state that is causing this appears to be fixed in that PR.
author_association: COLLABORATOR
user: login: camielvs
created_at: 2025-04-30T16:39:19Z
body: > NIT: some components have different size sheets. Maybe we should make it more consistent? either in this PR or make a fast follow P2 ticket <img alt="Screenshot 2025-04-30 at 11 28 04 AM" width="1687" src="https://private-user-images.githubusercontent.com/1576202/439301581-0a046f59-7f7d-4d57-8819-a5b495d75eec.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDYwMjc0NzYsIm5iZiI6MTc0NjAyNzE3NiwicGF0aCI6Ii8xNTc2MjAyLzQzOTMwMTU4MS0wYTA0NmY1OS03ZjdkLTRkNTctODgxOS1hNWI0OTVkNzVlZWMucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDQzMCUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTA0MzBUMTUzMjU2WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9NDI2ZDJiMTEwNDNjOTJjNDZmNzRkNDQyZmExZTUwNjFmZTRhN2Q1N2E1MDAxYjEzNmFlODNjOWRlNDY5MjkyNyZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.a-0Ot8s5fZmZKCwMWSLP16BhpjBuJf2vnR2AhvIxZ8o"> <img alt="Screenshot 2025-04-30 at 11 28 16 AM" width="1685" src="https://private-user-images.githubusercontent.com/1576202/439301645-c03a62f7-dd42-40d4-877a-e32ffe747d04.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDYwMjc0NzYsIm5iZiI6MTc0NjAyNzE3NiwicGF0aCI6Ii8xNTc2MjAyLzQzOTMwMTY0NS1jMDNhNjJmNy1kZDQyLTQwZDQtODc3YS1lMzJmZmU3NDdkMDQucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDQzMCUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTA0MzBUMTUzMjU2WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ZmJmZmZiNTU0NjQ2YzBiZDc1ODBmN2RkZjU4N2IzZTJhMmIzMTUxMGVlOWU4NTUyMWFiYTkxOTRkNDYwMjcyMCZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.Qazh6ysopC_hvDWAKlxrvvzEIg3jCNDuHA5FoFZXJE0">

Good catch! Should be fixed now
author_association: COLLABORATOR
user: login: camielvs
created_at: 2025-04-30T16:42:08Z
body: > Nit or fast follow up: <img alt="Screenshot 2025-04-30 at 11 30 33 AM" width="489" src="https://private-user-images.githubusercontent.com/1576202/439302451-07aff5e8-3d39-4f59-8d8a-908852bce8dc.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDYwMjc0NzYsIm5iZiI6MTc0NjAyNzE3NiwicGF0aCI6Ii8xNTc2MjAyLzQzOTMwMjQ1MS0wN2FmZjVlOC0zZDM5LTRmNTktOGQ4YS05MDg4NTJiY2U4ZGMucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDQzMCUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTA0MzBUMTUzMjU2WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ZTY1OGUwMGIzZWFmZmIyYzIwZjkzYzg4ZjE3MWI4MWRkMDZlZTE3N2MyYzg2MmU3NjNhODA5NTg0ZjQ3OGYxNSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.evHTCdtclTqFJjDsFtlNWsHWi6Xca9YTt9w3JuEo3GY">
> 
> The view repo should have a new window icon next to it. Does shadCN buttons or anything automatically support this?

I don't think it does. I will follow-up to create a `Link` component as a wrapper around `<a />` to do this and then we can use it through out the app instead
author_association: COLLABORATOR