Skip to content

Commit 17301a5

Browse files
committed
Rework Pipeline Action Buttons into Action Framework
1 parent 71fe8b0 commit 17301a5

File tree

5 files changed

+138
-162
lines changed

5 files changed

+138
-162
lines changed

src/components/Editor/Context/PipelineDetails.tsx

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
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";
1316
import useToastNotification from "@/hooks/useToastNotification";
1417
import { useComponentSpec } from "@/providers/ComponentSpecProvider";
15-
import { getComponentFileFromList } from "@/utils/componentStore";
18+
import { APP_ROUTES } from "@/routes/router";
19+
import {
20+
getComponentFileFromList,
21+
renameComponentFileInList,
22+
} from "@/utils/componentStore";
1623
import { USER_PIPELINES_LIST_NAME } from "@/utils/constants";
1724
import { componentSpecToText } from "@/utils/yaml";
1825

1926
import PipelineIO from "../../shared/Execution/PipelineIO";
2027
import { PipelineValidationList } from "./PipelineValidationList";
21-
import RenamePipeline from "./RenamePipeline";
2228

2329
const PipelineDetails = () => {
2430
const notify = useToastNotification();
@@ -27,12 +33,22 @@ const PipelineDetails = () => {
2733
digest,
2834
isComponentTreeValid,
2935
globalValidationIssues,
36+
saveComponentSpec,
3037
} = useComponentSpec();
3138

39+
const notify = useToastNotification();
40+
const navigate = useNavigate();
41+
42+
const location = useLocation();
43+
const pathname = location.pathname;
44+
45+
const title = componentSpec?.name;
46+
3247
const { handleIssueClick, groupedIssues } = useValidationIssueNavigation(
3348
globalValidationIssues,
3449
);
3550

51+
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
3652
const [isYamlFullscreen, setIsYamlFullscreen] = useState(false);
3753

3854
// State for file metadata
@@ -42,6 +58,31 @@ const PipelineDetails = () => {
4258
createdBy?: string;
4359
}>({});
4460

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

89-
const actions = [
90-
<RenamePipeline key="rename-pipeline-action" />,
91-
<TooltipButton
92-
variant="outline"
93-
tooltip="View YAML"
94-
onClick={() => setIsYamlFullscreen(true)}
95-
key="view-yaml-action"
96-
>
97-
<Icon name="FileCodeCorner" />
98-
</TooltipButton>,
130+
const actions: Action[] = [
131+
{
132+
label: "Rename Pipeline",
133+
icon: "PencilLine",
134+
onClick: () => setIsRenameDialogOpen(true),
135+
},
136+
{
137+
label: "View YAML",
138+
icon: "FileCodeCorner",
139+
onClick: () => setIsYamlFullscreen(true),
140+
},
99141
];
100142

101143
return (
@@ -151,6 +193,16 @@ const PipelineDetails = () => {
151193
onClose={() => setIsYamlFullscreen(false)}
152194
/>
153195
)}
196+
<PipelineNameDialog
197+
open={isRenameDialogOpen}
198+
onOpenChange={setIsRenameDialogOpen}
199+
title="Name Pipeline"
200+
description="Unsaved pipeline changes will be lost."
201+
initialName={title ?? ""}
202+
onSubmit={handleTitleUpdate}
203+
submitButtonText="Update Title"
204+
isSubmitDisabled={isSubmitDisabled}
205+
/>
154206
</>
155207
);
156208
};

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: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isValidElement, type ReactNode, useState } from "react";
1+
import { type ReactNode, useState } from "react";
22

33
import { Icon, type IconName } from "@/components/ui/icon";
44
import { BlockStack, InlineStack } from "@/components/ui/layout";
@@ -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,15 +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-
const key =
66-
isValidElement(action) && action.key != null
67-
? `action-node-${String(action.key)}`
68-
: `action-node-${index}`;
69-
return <span key={key}>{action}</span>;
70-
}
71-
60+
{actions.map((action) => {
7261
if (action.hidden) {
7362
return null;
7463
}

0 commit comments

Comments
 (0)