Skip to content

Commit d0dbbaa

Browse files
committed
feat: add Edit Metadata functionality with dialog for workflow metadata updates
1 parent ed50217 commit d0dbbaa

5 files changed

Lines changed: 203 additions & 4 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { useState } from "react";
2+
3+
import { Button } from "@/components/ui/button";
4+
import {
5+
Dialog,
6+
DialogContent,
7+
DialogFooter,
8+
DialogHeader,
9+
DialogTitle,
10+
} from "@/components/ui/dialog";
11+
import { Input } from "@/components/ui/input";
12+
import { Label } from "@/components/ui/label";
13+
import { Spinner } from "@/components/ui/spinner";
14+
import { Textarea } from "@/components/ui/textarea";
15+
16+
export interface EditMetadataDialogProps {
17+
open: boolean;
18+
onOpenChange: (open: boolean) => void;
19+
initialName: string;
20+
initialDescription?: string;
21+
onSave: (name: string, description?: string) => Promise<void>;
22+
}
23+
24+
export function EditMetadataDialog({
25+
open,
26+
onOpenChange,
27+
initialName,
28+
initialDescription,
29+
onSave,
30+
}: EditMetadataDialogProps) {
31+
const [workflowName, setWorkflowName] = useState(initialName);
32+
const [workflowDescription, setWorkflowDescription] = useState(
33+
initialDescription || ""
34+
);
35+
const [isSaving, setIsSaving] = useState(false);
36+
37+
// Reset state when dialog opens
38+
const handleOpenChange = (newOpen: boolean) => {
39+
if (newOpen) {
40+
setWorkflowName(initialName);
41+
setWorkflowDescription(initialDescription || "");
42+
}
43+
onOpenChange(newOpen);
44+
};
45+
46+
const handleSubmit = async (e: React.FormEvent) => {
47+
e.preventDefault();
48+
setIsSaving(true);
49+
try {
50+
await onSave(workflowName, workflowDescription || undefined);
51+
onOpenChange(false);
52+
} finally {
53+
setIsSaving(false);
54+
}
55+
};
56+
57+
return (
58+
<Dialog open={open} onOpenChange={handleOpenChange}>
59+
<DialogContent>
60+
<DialogHeader>
61+
<DialogTitle>Edit Metadata</DialogTitle>
62+
</DialogHeader>
63+
<form onSubmit={handleSubmit} className="space-y-4">
64+
<div>
65+
<Label htmlFor="workflow-name">Workflow Name</Label>
66+
<Input
67+
id="workflow-name"
68+
value={workflowName}
69+
onChange={(e) => setWorkflowName(e.target.value)}
70+
placeholder="Enter workflow name"
71+
className="mt-2"
72+
/>
73+
</div>
74+
<div>
75+
<Label htmlFor="workflow-description">Description (Optional)</Label>
76+
<Textarea
77+
id="workflow-description"
78+
value={workflowDescription}
79+
onChange={(e) => setWorkflowDescription(e.target.value)}
80+
placeholder="Describe what you are building"
81+
className="mt-2"
82+
maxLength={256}
83+
rows={3}
84+
/>
85+
</div>
86+
<DialogFooter>
87+
<Button
88+
variant="outline"
89+
type="button"
90+
onClick={() => onOpenChange(false)}
91+
disabled={isSaving}
92+
>
93+
Cancel
94+
</Button>
95+
<Button type="submit" disabled={isSaving}>
96+
{isSaving ? <Spinner className="h-4 w-4 mr-2" /> : null}
97+
Save
98+
</Button>
99+
</DialogFooter>
100+
</form>
101+
</DialogContent>
102+
</Dialog>
103+
);
104+
}

apps/web/src/components/workflow/workflow-builder.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface WorkflowBuilderProps {
4848
initialWorkflowExecution?: WorkflowExecution;
4949
readonly?: boolean;
5050
onDeployWorkflow?: (e: React.MouseEvent) => void;
51+
onEditMetadata?: (e: React.MouseEvent) => void;
5152
onSetSchedule?: () => void;
5253
onShowHttpIntegration?: () => void;
5354
onShowEmailTrigger?: () => void;
@@ -68,6 +69,7 @@ export function WorkflowBuilder({
6869
initialWorkflowExecution,
6970
readonly = false,
7071
onDeployWorkflow,
72+
onEditMetadata,
7173
onSetSchedule,
7274
onShowHttpIntegration,
7375
onShowEmailTrigger,
@@ -443,6 +445,9 @@ export function WorkflowBuilder({
443445
onDeploy={
444446
!readonly && onDeployWorkflow ? onDeployWorkflow : undefined
445447
}
448+
onEditMetadata={
449+
!readonly && onEditMetadata ? onEditMetadata : undefined
450+
}
446451
workflowStatus={workflowStatus}
447452
workflowType={workflowType}
448453
onSetSchedule={onSetSchedule}

apps/web/src/components/workflow/workflow-canvas.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import Clock from "lucide-react/icons/clock";
2323
import Copy from "lucide-react/icons/copy";
2424
import Eye from "lucide-react/icons/eye";
2525
import EyeOff from "lucide-react/icons/eye-off";
26+
import FileText from "lucide-react/icons/file-text";
2627
import Globe from "lucide-react/icons/globe";
2728
import Layers2 from "lucide-react/icons/layers-2";
2829
import Mail from "lucide-react/icons/mail";
@@ -166,6 +167,7 @@ export interface WorkflowCanvasProps {
166167
onAddNode?: () => void;
167168
onAction?: (e: React.MouseEvent) => void;
168169
onDeploy?: (e: React.MouseEvent) => void;
170+
onEditMetadata?: (e: React.MouseEvent) => void;
169171
onSetSchedule?: () => void;
170172
onShowHttpIntegration?: () => void;
171173
onShowEmailTrigger?: () => void;
@@ -312,6 +314,27 @@ export function DeployButton({
312314
);
313315
}
314316

317+
export function EditMetadataButton({
318+
onClick,
319+
className = "",
320+
tooltip = "Edit Metadata",
321+
}: {
322+
onClick: (e: React.MouseEvent) => void;
323+
className?: string;
324+
tooltip?: string;
325+
}) {
326+
return (
327+
<ActionBarButton
328+
onClick={onClick}
329+
tooltip={tooltip}
330+
tooltipSide="bottom"
331+
className={cn(actionBarButtonOutlineClassName, className)}
332+
>
333+
<FileText className="!size-4" />
334+
</ActionBarButton>
335+
);
336+
}
337+
315338
type SidebarToggleProps = {
316339
onClick: (e: React.MouseEvent) => void;
317340
isSidebarVisible: boolean;
@@ -704,6 +727,7 @@ export function WorkflowCanvas({
704727
onAddNode,
705728
onAction,
706729
onDeploy,
730+
onEditMetadata,
707731
workflowType,
708732
onSetSchedule,
709733
onShowHttpIntegration,
@@ -849,6 +873,9 @@ export function WorkflowCanvas({
849873
disabled={nodes.length === 0}
850874
/>
851875
)}
876+
{onEditMetadata && (
877+
<EditMetadataButton onClick={onEditMetadata} />
878+
)}
852879
{onSetSchedule && workflowType === "cron" && (
853880
<SetScheduleButton
854881
onClick={onSetSchedule}

apps/web/src/pages/editor-page.tsx

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { toast } from "sonner";
77

88
import { useAuth } from "@/components/auth-context";
99
import { InsetLoading } from "@/components/inset-loading";
10+
import { EditMetadataDialog } from "@/components/workflow/edit-metadata-dialog";
1011
import { EmailTriggerDialog } from "@/components/workflow/email-trigger-dialog";
1112
import { ExecutionEmailDialog } from "@/components/workflow/execution-email-dialog";
1213
import { ExecutionFormDialog } from "@/components/workflow/execution-form-dialog";
@@ -34,6 +35,8 @@ import {
3435
import { useObjectService } from "@/services/object-service";
3536
import { useNodeTypes } from "@/services/type-service";
3637
import {
38+
getWorkflow,
39+
updateWorkflow,
3740
upsertCronTrigger,
3841
useCronTrigger,
3942
useWorkflowExecution,
@@ -53,6 +56,11 @@ export function EditorPage() {
5356
useState(false);
5457
const [isEmailTriggerDialogOpen, setIsEmailTriggerDialogOpen] =
5558
useState(false);
59+
const [isEditMetadataDialogOpen, setIsEditMetadataDialogOpen] =
60+
useState(false);
61+
const [localWorkflowName, setLocalWorkflowName] = useState<string>("");
62+
const [localWorkflowDescription, setLocalWorkflowDescription] =
63+
useState<string>();
5664

5765
// Fetch all node types initially (no filter)
5866
const { nodeTypes, nodeTypesError, isNodeTypesLoading } = useNodeTypes(
@@ -220,12 +228,20 @@ export function EditorPage() {
220228
submitEmailFormData,
221229
} = useWorkflowExecution(orgHandle, wsExecuteWorkflow);
222230

231+
// Sync local workflow name from metadata
232+
useEffect(() => {
233+
if (workflowMetadata?.name && !localWorkflowName) {
234+
setLocalWorkflowName(workflowMetadata.name);
235+
setLocalWorkflowDescription((workflowMetadata as any).description);
236+
}
237+
}, [workflowMetadata, localWorkflowName]);
238+
223239
usePageBreadcrumbs(
224240
[
225241
{ label: "Workflows", to: getOrgUrl("workflows") },
226-
{ label: workflowMetadata?.name || "Workflow" },
242+
{ label: localWorkflowName || workflowMetadata?.name || "Workflow" },
227243
],
228-
[workflowMetadata?.name]
244+
[localWorkflowName, workflowMetadata?.name]
229245
);
230246

231247
const validateConnection = useCallback(
@@ -280,6 +296,42 @@ export function EditorPage() {
280296
}
281297
};
282298

299+
const handleEditMetadata = useCallback((e: React.MouseEvent) => {
300+
e.preventDefault();
301+
e.stopPropagation();
302+
setIsEditMetadataDialogOpen(true);
303+
}, []);
304+
305+
const handleSaveMetadata = useCallback(
306+
async (name: string, description?: string) => {
307+
if (!id || !orgHandle || !workflowMetadata) return;
308+
309+
try {
310+
const fullWorkflow = await getWorkflow(id, orgHandle);
311+
await updateWorkflow(
312+
id,
313+
{
314+
name,
315+
description,
316+
type: fullWorkflow.type,
317+
nodes: fullWorkflow.nodes,
318+
edges: fullWorkflow.edges,
319+
},
320+
orgHandle
321+
);
322+
toast.success("Workflow metadata updated");
323+
324+
// Update local state for display
325+
setLocalWorkflowName(name);
326+
setLocalWorkflowDescription(description);
327+
} catch (error) {
328+
console.error("Error updating workflow metadata:", error);
329+
toast.error("Failed to update workflow metadata");
330+
}
331+
},
332+
[id, orgHandle, workflowMetadata]
333+
);
334+
283335
const handleDeployWorkflow = useCallback(
284336
async (e: React.MouseEvent) => {
285337
e.preventDefault();
@@ -382,6 +434,7 @@ export function EditorPage() {
382434
validateConnection={validateConnection}
383435
executeWorkflow={editorExecuteWorkflow}
384436
onDeployWorkflow={handleDeployWorkflow}
437+
onEditMetadata={handleEditMetadata}
385438
createObjectUrl={createObjectUrl}
386439
/>
387440
</div>
@@ -449,6 +502,15 @@ export function EditorPage() {
449502
workflowName={workflowMetadata?.name}
450503
/>
451504
)}
505+
{workflowMetadata && (
506+
<EditMetadataDialog
507+
open={isEditMetadataDialogOpen}
508+
onOpenChange={setIsEditMetadataDialogOpen}
509+
initialName={localWorkflowName || workflowMetadata.name}
510+
initialDescription={localWorkflowDescription}
511+
onSave={handleSaveMetadata}
512+
/>
513+
)}
452514
</div>
453515
</ReactFlowProvider>
454516
);

apps/web/src/pages/workflows-page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ function useWorkflowActions() {
6565
const [workflowToDeploy, setWorkflowToDeploy] =
6666
useState<WorkflowWithMetadata | null>(null);
6767
const [renameWorkflowName, setRenameWorkflowName] = useState("");
68-
const [renameWorkflowDescription, setRenameWorkflowDescription] = useState("");
68+
const [renameWorkflowDescription, setRenameWorkflowDescription] =
69+
useState("");
6970
const [isDeleting, setIsDeleting] = useState(false);
7071
const [isRenaming, setIsRenaming] = useState(false);
7172
const [isDeploying, setIsDeploying] = useState(false);
@@ -163,7 +164,7 @@ function useWorkflowActions() {
163164
<Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}>
164165
<DialogContent>
165166
<DialogHeader>
166-
<DialogTitle>Edit Workflow</DialogTitle>
167+
<DialogTitle>Edit Metadata</DialogTitle>
167168
</DialogHeader>
168169
<form onSubmit={handleRenameWorkflow} className="space-y-4">
169170
<div>

0 commit comments

Comments
 (0)