add browser session id to workflow run form (#2686)

This commit is contained in:
Shuchang Zheng
2025-06-11 12:49:01 -07:00
committed by GitHub
parent c1e19d27d3
commit 2d2146948a
3 changed files with 107 additions and 3 deletions

View File

@@ -0,0 +1,20 @@
/**
* Returns a value from localStorage for the given key, or a fallback if not present.
* Use this hook to initialize form default values from localStorage in a type-safe way.
*
* @param storageKey - The localStorage key to read
* @param fallback - The fallback value if localStorage is empty or unavailable
* @returns The value from localStorage (if present), otherwise the fallback
*/
import { useMemo } from "react";
export function useLocalStorageFormDefault(
storageKey: string,
fallback: string | null | undefined,
): string | null | undefined {
return useMemo(() => {
if (typeof window === "undefined") return fallback ?? null;
const value = localStorage.getItem(storageKey);
return value !== null ? value : fallback ?? null;
}, [storageKey, fallback]);
}

View File

@@ -0,0 +1,23 @@
import { useEffect } from "react";
import { UseFormReturn, FieldValues, WatchObserver } from "react-hook-form";
/**
* Syncs a form field value to localStorage whenever it changes.
* @param form - A react-hook-form object with a .watch method
* @param fieldName - The name of the field to watch
* @param storageKey - The key to write to in localStorage
*/
export function useSyncFormFieldToStorage<T extends FieldValues>(
form: UseFormReturn<T>,
fieldName: keyof T & string,
storageKey: string,
) {
useEffect(() => {
const subscription = form.watch(((value, { name }) => {
if (name === fieldName && typeof value[fieldName] === "string") {
localStorage.setItem(storageKey, value[fieldName] as string);
}
}) as WatchObserver<T>);
return () => subscription.unsubscribe();
}, [form, fieldName, storageKey]);
}

View File

@@ -14,6 +14,8 @@ import { Input } from "@/components/ui/input";
import { toast } from "@/components/ui/use-toast";
import { useApiCredential } from "@/hooks/useApiCredential";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { useSyncFormFieldToStorage } from "@/hooks/useSyncFormFieldToStorage";
import { useLocalStorageFormDefault } from "@/hooks/useLocalStorageFormDefault";
import { apiBaseUrl } from "@/util/env";
import { type ApiCommandOptions } from "@/util/apiCommands";
import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
@@ -70,22 +72,27 @@ type RunWorkflowRequestBody = {
data: Record<string, unknown>; // workflow parameters and values
proxy_location: ProxyLocation | null;
webhook_callback_url?: string | null;
browser_session_id: string | null;
};
function getRunWorkflowRequestBody(
values: RunWorkflowFormType,
workflowParameters: Array<WorkflowParameter>,
): RunWorkflowRequestBody {
const { webhookCallbackUrl, proxyLocation, ...parameters } = values;
const { webhookCallbackUrl, proxyLocation, browserSessionId, ...parameters } =
values;
const parsedParameters = parseValuesForWorkflowRun(
parameters,
workflowParameters,
);
const bsi = browserSessionId?.trim() === "" ? null : browserSessionId;
const body: RunWorkflowRequestBody = {
data: parsedParameters,
proxy_location: proxyLocation,
browser_session_id: bsi,
};
if (webhookCallbackUrl) {
@@ -98,6 +105,7 @@ function getRunWorkflowRequestBody(
type RunWorkflowFormType = Record<string, unknown> & {
webhookCallbackUrl: string;
proxyLocation: ProxyLocation;
browserSessionId: string | null;
};
function RunWorkflowForm({
@@ -109,15 +117,26 @@ function RunWorkflowForm({
const credentialGetter = useCredentialGetter();
const navigate = useNavigate();
const queryClient = useQueryClient();
const browserSessionIdDefault = useLocalStorageFormDefault(
"skyvern.browserSessionId",
(initialValues.browserSessionId as string | undefined) ?? null,
);
const form = useForm<RunWorkflowFormType>({
defaultValues: {
...initialValues,
webhookCallbackUrl: initialSettings.webhookCallbackUrl,
proxyLocation: initialSettings.proxyLocation,
browserSessionId: browserSessionIdDefault,
},
});
const apiCredential = useApiCredential();
useSyncFormFieldToStorage(
form,
"browserSessionId",
"skyvern.browserSessionId",
);
const runWorkflowMutation = useMutation({
mutationFn: async (values: RunWorkflowFormType) => {
const client = await getClient(credentialGetter);
@@ -154,7 +173,13 @@ function RunWorkflowForm({
});
function onSubmit(values: RunWorkflowFormType) {
const { webhookCallbackUrl, proxyLocation, ...parameters } = values;
const {
webhookCallbackUrl,
proxyLocation,
browserSessionId,
...parameters
} = values;
const parsedParameters = parseValuesForWorkflowRun(
parameters,
workflowParameters,
@@ -163,6 +188,7 @@ function RunWorkflowForm({
...parsedParameters,
webhookCallbackUrl,
proxyLocation,
browserSessionId,
});
}
@@ -330,8 +356,43 @@ function RunWorkflowForm({
);
}}
/>
<FormField
key="browserSessionId"
control={form.control}
name="browserSessionId"
render={({ field }) => {
return (
<FormItem>
<div className="flex gap-16">
<FormLabel>
<div className="w-72">
<div className="flex items-center gap-2 text-lg">
Browser Session ID
</div>
<h2 className="text-sm text-slate-400">
Use a persistent browser session to maintain state and
enable browser interaction.
</h2>
</div>
</FormLabel>
<div className="w-full space-y-2">
<FormControl>
<Input
{...field}
placeholder="pbs_xxx"
value={
field.value === null ? "" : (field.value as string)
}
/>
</FormControl>
<FormMessage />
</div>
</div>
</FormItem>
);
}}
/>
</div>
<div className="flex justify-end gap-2">
<CopyApiCommandDropdown
getOptions={() => {