Skip to content

Commit

Permalink
fix(ui): Fix uncontrolled input to be controlled warning
Browse files Browse the repository at this point in the history
  • Loading branch information
topher-lo committed Mar 4, 2024
1 parent adfedfd commit 7129bf1
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 75 deletions.
2 changes: 1 addition & 1 deletion frontend/src/components/action-node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export default React.memo(function ActionNode({
<CardContent className="pt-0 pb-4 pl-5 pr-5">
<div className="flex space-x-4 text-xs text-muted-foreground">
<div className="flex items-center">
{status === 'online' && (
{status === "online" && (
<CircleIcon className="mr-1 h-3 w-3 fill-green-400 text-green-400" />
)}
{status === 'offline' && (
Expand Down
107 changes: 72 additions & 35 deletions frontend/src/components/forms/action.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";

import axios from "axios";
import { useQuery } from "@tanstack/react-query";
import { useQuery, useMutation } from "@tanstack/react-query";
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
Expand All @@ -18,6 +18,7 @@ import {
} from "@/components/ui/form"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "@/components/ui/separator"
import { Skeleton } from "@/components/ui/skeleton"
import { Textarea } from "@/components/ui/textarea"
import { Input } from "@/components/ui/input"
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"
Expand All @@ -28,14 +29,10 @@ import { CircleIcon, Save } from "lucide-react"
// Define formSchema for type safety
const actionFormSchema = z.object({
name: z.string(),
description: z.string()
description: z.string(),
inputs: z.string().optional(),
})

interface ActionFormData {
actionId: string | null; // Temporary: String should be enforced in panel.tsx...
actionData: any;
}

interface ActionResponse {
id: string;
title: string;
Expand All @@ -44,12 +41,16 @@ interface ActionResponse {
inputs: Record<string, any> | null;
}

interface ActionFormProps {
actionId: string;
}

export function ActionForm({ actionId, actionData }: ActionFormData) {
export function ActionForm({ actionId }: ActionFormProps): React.JSX.Element {

const { selectedWorkflowMetadata, setSelectedWorkflowMetadata } = useSelectedWorkflowMetadata();
const workflowId = selectedWorkflowMetadata.id;

// Fetch Action by ID and Workflow ID
const getActionById = async (): Promise<ActionResponse> => {
try {
const response = await axios.get<ActionResponse>(`http://localhost:8000/actions/${actionId}?workflow_id=${workflowId}`);
Expand All @@ -68,6 +69,10 @@ export function ActionForm({ actionId, actionData }: ActionFormData) {
const [status, setStatus] = useState("offline");
const form = useForm<z.infer<typeof actionFormSchema>>({
resolver: zodResolver(actionFormSchema),
defaultValues: {
name: "", // Default value for name
description: "", // Default value for description
},
});

useEffect(() => {
Expand All @@ -80,37 +85,69 @@ export function ActionForm({ actionId, actionData }: ActionFormData) {
setStatus(status);
}
}, [data, form.reset]);

const statusCapitalized = status[0].toUpperCase() + status.slice(1);

// Submit form and update Action
async function updateAction(actionId: string, values: z.infer<typeof actionFormSchema>) {
const response = await axios.post(`http://localhost:8000/actions/${actionId}`, values);
return response.data; // Adjust based on what your API returns
}

function useUpdateAction(actionId: string) {
const mutation = useMutation({
mutationFn: (values: z.infer<typeof actionFormSchema>) => updateAction(actionId, values),
// Configure your mutation behavior here
onSuccess: (data, variables, context) => {
console.log("Action update successful", data);
},
onError: (error, variables, context) => {
console.error("Failed to update action:", error);
},
});

return mutation;
}

const { mutate } = useUpdateAction(actionId);
function onSubmit(values: z.infer<typeof actionFormSchema>) {
console.log(values)
mutate(values);
}

if (!data) {
return (
<div className="flex items-center space-x-2 p-4">
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
)
}

return (
<ScrollArea>
<div className="space-y-4 p-4">
<div className="space-y-3">
<h4 className="text-sm font-medium">Action Status</h4>
<div className="flex justify-between">
<Badge variant="outline" className={`py-1 px-4 ${status === 'online' ? 'bg-green-100' : 'bg-gray-100'}`}>
<CircleIcon className={`mr-2 h-3 w-3 ${status === 'online' ? 'fill-green-600 text-green-600' : 'fill-gray-400 text-gray-400'}`} />
<span className={`text-muted-foreground ${status === 'online' ? 'text-green-600' : 'text-gray-600'}`}>{statusCapitalized}</span>
</Badge>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon">
<Save className="h-4 w-4" />
<span className="sr-only">Save</span>
</Button>
</TooltipTrigger>
<TooltipContent>Save</TooltipContent>
</Tooltip>
</div>
</div>
<Separator />
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="space-y-4 p-4">
<div className="space-y-3">
<h4 className="text-sm font-medium">Action Status</h4>
<div className="flex justify-between">
<Badge variant="outline" className={`py-1 px-4 ${status === "online" ? 'bg-green-100' : 'bg-gray-100'}`}>
<CircleIcon className={`mr-2 h-3 w-3 ${status === "online" ? 'fill-green-600 text-green-600' : 'fill-gray-400 text-gray-400'}`} />
<span className={`text-muted-foreground ${status === "online" ? 'text-green-600' : 'text-gray-600'}`}>{statusCapitalized}</span>
</Badge>
<Tooltip>
<TooltipTrigger asChild>
<Button type="submit" size="icon">
<Save className="h-4 w-4" />
<span className="sr-only">Save</span>
</Button>
</TooltipTrigger>
<TooltipContent>Save</TooltipContent>
</Tooltip>
</div>
</div>
<Separator />
<div className="space-y-4 mb-4">
<FormField
control={form.control}
Expand Down Expand Up @@ -146,9 +183,9 @@ export function ActionForm({ actionId, actionData }: ActionFormData) {
</p>
</div>
</div>
</form>
</Form>
</div>
</div>
</form>
</Form>
</ScrollArea>
)
}
84 changes: 56 additions & 28 deletions frontend/src/components/forms/workflow.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import axios from "axios";
import { useMutation } from "@tanstack/react-query";

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
Expand All @@ -13,6 +16,7 @@ import {
FormMessage,
} from "@/components/ui/form"
import { Separator } from "@/components/ui/separator"
import { Skeleton } from "@/components/ui/skeleton"
import { Textarea } from "@/components/ui/textarea"
import { Input } from "@/components/ui/input"
import { CircleIcon, Save } from "lucide-react"
Expand All @@ -21,11 +25,12 @@ import { useSelectedWorkflowMetadata } from "@/providers/selected-workflow"

// Define formSchema for type safety
const workflowFormSchema = z.object({
name: z.string(),
title: z.string(),
description: z.string()
})

export function WorkflowForm() {

export function WorkflowForm(): React.JSX.Element {

const { selectedWorkflowMetadata, setSelectedWorkflowMetadata } = useSelectedWorkflowMetadata()
const status = selectedWorkflowMetadata.status || "offline"
Expand All @@ -34,42 +39,65 @@ export function WorkflowForm() {
const form = useForm<z.infer<typeof workflowFormSchema>>({
resolver: zodResolver(workflowFormSchema),
defaultValues: {
name: selectedWorkflowMetadata.title || "",
title: selectedWorkflowMetadata.title || "",
description: selectedWorkflowMetadata.description || "",
},
})

// Submit form and update Workflow
async function updateWorkflow(workflowId: string, values: z.infer<typeof workflowFormSchema>) {
const response = await axios.post(`http://localhost:8000/workflows/${workflowId}`, values);
return response.data; // Adjust based on what your API returns
}

function useUpdateWorkflow(workflowId: string) {
const mutation = useMutation({
mutationFn: (values: z.infer<typeof workflowFormSchema>) => updateWorkflow(workflowId, values),
// Configure your mutation behavior here
onSuccess: (data, variables, context) => {
console.log("Workflow update successful", data);
},
onError: (error, variables, context) => {
console.error("Failed to update workflow:", error);
},
});

return mutation;
}

// TODO: Move get workflow ID logic into panel to ensure order of hooks called
const { mutate } = useUpdateWorkflow(selectedWorkflowMetadata.id);
function onSubmit(values: z.infer<typeof workflowFormSchema>) {
console.log(values)
mutate(values);
}

return (
<Form {...form}>
<div className="space-y-4 p-4">
<div className="space-y-3">
<h4 className="text-sm font-medium">Workflow Status</h4>
<div className="flex justify-between">
<Badge variant="outline" className={`py-1 px-4 ${status === 'online' ? 'bg-green-100' : 'bg-gray-100'}`}>
<CircleIcon className={`mr-2 h-3 w-3 ${status === 'online' ? 'fill-green-600 text-green-600' : 'fill-gray-400 text-gray-400'}`} />
<span className={ `text-muted-foreground ${status === 'online' ? 'text-green-600' : 'text-gray-600'}`}>{statusCapitalized}</span>
</Badge>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon">
<Save className="h-4 w-4" />
<span className="sr-only">Save</span>
</Button>
</TooltipTrigger>
<TooltipContent>Archive</TooltipContent>
</Tooltip>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div className="space-y-4 p-4">
<div className="space-y-3">
<h4 className="text-sm font-medium">Workflow Status</h4>
<div className="flex justify-between">
<Badge variant="outline" className={`py-1 px-4 ${status === "online" ? 'bg-green-100' : 'bg-gray-100'}`}>
<CircleIcon className={`mr-2 h-3 w-3 ${status === "online" ? 'fill-green-600 text-green-600' : 'fill-gray-400 text-gray-400'}`} />
<span className={`text-muted-foreground ${status === "online" ? 'text-green-600' : 'text-gray-600'}`}>{statusCapitalized}</span>
</Badge>
<Tooltip>
<TooltipTrigger asChild>
<Button type="submit" size="icon">
<Save className="h-4 w-4" />
<span className="sr-only">Save</span>
</Button>
</TooltipTrigger>
<TooltipContent>Save</TooltipContent>
</Tooltip>
</div>
</div>
</div>
<Separator />
<div className="space-y-4">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<Separator />
<div className="space-y-4">
<FormField
control={form.control}
name="name"
name="title"
render={({ field }: { field: any }) => (
<FormItem>
<FormLabel>Name</FormLabel>
Expand All @@ -93,9 +121,9 @@ export function WorkflowForm() {
</FormItem>
)}
/>
</form>
</div>
</div>
</div>
</form>
</Form>
)
}
32 changes: 26 additions & 6 deletions frontend/src/components/panel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState } from "react";
import { useOnSelectionChange, Node } from "reactflow";
import { Skeleton } from "@/components/ui/skeleton"

import { WorkflowForm } from "@/components/forms/workflow"
import { ActionForm } from "@/components/forms/action"
Expand All @@ -8,31 +9,50 @@ export function WorkflowPanel() {

const [isActionNodeSelected, setIsActionNodeSelected] = useState(false);
const [selectedActionNodeId, setSelectedActionNodeId] = useState<string | null>(null)
const [selectedActionNodeData, setSelectedActionNodeData] = useState<any | null>(null)


useOnSelectionChange({
onChange: ({ nodes }: { nodes: Node[] }) => {
const actionNodeSelected = nodes.find((node: Node) => node.type === 'action');
const actionNodeSelected = nodes.find((node: Node) => node.type === "action");
if (actionNodeSelected) {
setIsActionNodeSelected(true);
setSelectedActionNodeId(actionNodeSelected.id);
setSelectedActionNodeData(actionNodeSelected.data);
} else {
setIsActionNodeSelected(false);
setSelectedActionNodeId(null);
setSelectedActionNodeData(null);
}
}
});

if (isActionNodeSelected && !selectedActionNodeId) {
return (
<div className="flex flex-col h-full">
<div className="flex-1 flex">
<div className="flex-1">
<div className="flex items-center space-x-2 p-4">
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
</div>
</div>
</div>
)
}

return (
<div className="flex flex-col h-full">
<div className="flex-1 flex">
<div className="flex-1">
{isActionNodeSelected ? <ActionForm actionId={selectedActionNodeId} actionData={selectedActionNodeData} /> : <WorkflowForm />}
{isActionNodeSelected && selectedActionNodeId ? (
// Make sure selectedActionNodeId is a string when passed as a prop
<ActionForm actionId={selectedActionNodeId} />
) : (
<WorkflowForm />
)}
</div>
</div>
</div>
)
);
}
15 changes: 15 additions & 0 deletions frontend/src/components/ui/skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"

function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
)
}

export { Skeleton }
Loading

0 comments on commit 7129bf1

Please sign in to comment.