Skip to content

Commit f3bd2dd

Browse files
committed
Rework Pipeline Action Buttons into Action Framework
1 parent 64a6bf9 commit f3bd2dd

File tree

5 files changed

+138
-157
lines changed

5 files changed

+138
-157
lines changed

src/components/Editor/Context/PipelineDetails.tsx

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,53 @@
1+
import { useLocation, useNavigate } from "@tanstack/react-router";
12
import { useEffect, useState } from "react";
23

34
import { useValidationIssueNavigation } from "@/components/Editor/hooks/useValidationIssueNavigation";
4-
import TooltipButton from "@/components/shared/Buttons/TooltipButton";
55
import { CodeViewer } from "@/components/shared/CodeViewer";
6-
import { ActionBlock } from "@/components/shared/ContextPanel/Blocks/ActionBlock";
6+
import {
7+
type Action,
8+
ActionBlock,
9+
} from "@/components/shared/ContextPanel/Blocks/ActionBlock";
710
import { ContentBlock } from "@/components/shared/ContextPanel/Blocks/ContentBlock";
811
import { ListBlock } from "@/components/shared/ContextPanel/Blocks/ListBlock";
912
import { TextBlock } from "@/components/shared/ContextPanel/Blocks/TextBlock";
1013
import { CopyText } from "@/components/shared/CopyText/CopyText";
11-
import { Icon } from "@/components/ui/icon";
14+
import { PipelineNameDialog } from "@/components/shared/Dialogs";
1215
import { BlockStack } from "@/components/ui/layout";
16+
import useToastNotification from "@/hooks/useToastNotification";
1317
import { useComponentSpec } from "@/providers/ComponentSpecProvider";
14-
import { getComponentFileFromList } from "@/utils/componentStore";
18+
import { APP_ROUTES } from "@/routes/router";
19+
import {
20+
getComponentFileFromList,
21+
renameComponentFileInList,
22+
} from "@/utils/componentStore";
1523
import { USER_PIPELINES_LIST_NAME } from "@/utils/constants";
1624
import { componentSpecToText } from "@/utils/yaml";
1725

1826
import PipelineIO from "../../shared/Execution/PipelineIO";
1927
import { PipelineValidationList } from "./PipelineValidationList";
20-
import RenamePipeline from "./RenamePipeline";
2128

2229
const PipelineDetails = () => {
2330
const {
2431
componentSpec,
2532
digest,
2633
isComponentTreeValid,
2734
globalValidationIssues,
35+
saveComponentSpec,
2836
} = useComponentSpec();
2937

38+
const notify = useToastNotification();
39+
const navigate = useNavigate();
40+
41+
const location = useLocation();
42+
const pathname = location.pathname;
43+
44+
const title = componentSpec?.name;
45+
3046
const { handleIssueClick, groupedIssues } = useValidationIssueNavigation(
3147
globalValidationIssues,
3248
);
3349

50+
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
3451
const [isYamlFullscreen, setIsYamlFullscreen] = useState(false);
3552

3653
// State for file metadata
@@ -40,6 +57,31 @@ const PipelineDetails = () => {
4057
createdBy?: string;
4158
}>({});
4259

60+
const isSubmitDisabled = (name: string) => {
61+
return name === title;
62+
};
63+
64+
const handleTitleUpdate = async (name: string) => {
65+
if (!componentSpec) {
66+
notify("Update failed: ComponentSpec not found", "error");
67+
return;
68+
}
69+
70+
await renameComponentFileInList(
71+
USER_PIPELINES_LIST_NAME,
72+
title ?? "",
73+
name,
74+
pathname,
75+
);
76+
77+
await saveComponentSpec(name);
78+
79+
const urlName = encodeURIComponent(name);
80+
const url = APP_ROUTES.PIPELINE_EDITOR.replace("$name", urlName);
81+
82+
navigate({ to: url });
83+
};
84+
4385
// Fetch file metadata on mount or when componentSpec.name changes
4486
useEffect(() => {
4587
const fetchMeta = async () => {
@@ -78,16 +120,17 @@ const PipelineDetails = () => {
78120
componentSpec.metadata?.annotations || {},
79121
).map(([key, value]) => ({ label: key, value: String(value) }));
80122

81-
const actions = [
82-
<RenamePipeline key="rename-pipeline-action" />,
83-
<TooltipButton
84-
variant="outline"
85-
tooltip="View YAML"
86-
onClick={() => setIsYamlFullscreen(true)}
87-
key="view-yaml-action"
88-
>
89-
<Icon name="FileCodeCorner" />
90-
</TooltipButton>,
123+
const actions: Action[] = [
124+
{
125+
label: "Rename Pipeline",
126+
icon: "PencilLine",
127+
onClick: () => setIsRenameDialogOpen(true),
128+
},
129+
{
130+
label: "View YAML",
131+
icon: "FileCodeCorner",
132+
onClick: () => setIsYamlFullscreen(true),
133+
},
91134
];
92135

93136
return (
@@ -143,6 +186,16 @@ const PipelineDetails = () => {
143186
onClose={() => setIsYamlFullscreen(false)}
144187
/>
145188
)}
189+
<PipelineNameDialog
190+
open={isRenameDialogOpen}
191+
onOpenChange={setIsRenameDialogOpen}
192+
title="Name Pipeline"
193+
description="Unsaved pipeline changes will be lost."
194+
initialName={title ?? ""}
195+
onSubmit={handleTitleUpdate}
196+
submitButtonText="Update Title"
197+
isSubmitDisabled={isSubmitDisabled}
198+
/>
146199
</>
147200
);
148201
};

src/components/Editor/Context/RenamePipeline.tsx

Lines changed: 0 additions & 64 deletions
This file was deleted.

src/components/shared/ContextPanel/Blocks/ActionBlock.test.tsx

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -99,34 +99,6 @@ describe("<ActionBlock />", () => {
9999
expect(screen.getByTestId("action-Action 2")).toBeInTheDocument();
100100
expect(screen.getByTestId("action-Action 3")).toBeInTheDocument();
101101
});
102-
103-
test("renders ReactNode as action (backward compatibility)", () => {
104-
const actions = [
105-
{ label: "Action 1", icon: "Copy" as const, onClick: vi.fn() },
106-
<button key="custom" data-testid="custom-button">
107-
Custom Button
108-
</button>,
109-
];
110-
111-
render(<ActionBlock actions={actions} />);
112-
113-
expect(screen.getByTestId("action-Action 1")).toBeInTheDocument();
114-
expect(screen.getByTestId("custom-button")).toBeInTheDocument();
115-
});
116-
117-
test("handles null or undefined actions gracefully", () => {
118-
const actions = [
119-
{ label: "Action 1", icon: "Copy" as const, onClick: vi.fn() },
120-
null,
121-
undefined,
122-
{ label: "Action 2", icon: "Download" as const, onClick: vi.fn() },
123-
];
124-
125-
render(<ActionBlock actions={actions} />);
126-
127-
expect(screen.getByTestId("action-Action 1")).toBeInTheDocument();
128-
expect(screen.getByTestId("action-Action 2")).toBeInTheDocument();
129-
});
130102
});
131103

132104
describe("action variants", () => {

src/components/shared/ContextPanel/Blocks/ActionBlock.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,9 @@ export type Action = {
2020
| { content: ReactNode; icon?: never }
2121
);
2222

23-
// Temporary: ReactNode included for backward compatibility with some existing buttons. In the long-term we should strive for only Action types.
24-
export type ActionOrReactNode = Action | ReactNode;
25-
2623
interface ActionBlockProps {
2724
title?: string;
28-
actions: ActionOrReactNode[];
25+
actions: Action[];
2926
className?: string;
3027
}
3128

@@ -60,11 +57,7 @@ export const ActionBlock = ({
6057
<BlockStack className={className}>
6158
{title && <Heading level={3}>{title}</Heading>}
6259
<InlineStack gap="2">
63-
{actions.map((action, index) => {
64-
if (!action || typeof action !== "object" || !("label" in action)) {
65-
return <div key={index}>{action}</div>;
66-
}
67-
60+
{actions.map((action) => {
6861
if (action.hidden) {
6962
return null;
7063
}

0 commit comments

Comments
 (0)