improvements for folders and parameters (#3918)

Co-authored-by: Jonathan Dobson <jon.m.dobson@gmail.com>
This commit is contained in:
Celal Zamanoglu
2025-11-06 20:09:26 +03:00
committed by GitHub
parent d104135025
commit dea70f2782
10 changed files with 106 additions and 13 deletions

View File

@@ -35,9 +35,13 @@ function getErrorMessage(error: unknown, fallback: string): string {
interface ImportWorkflowButtonProps {
onImportStart?: () => void;
selectedFolderId?: string | null;
}
function ImportWorkflowButton({ onImportStart }: ImportWorkflowButtonProps) {
function ImportWorkflowButton({
onImportStart,
selectedFolderId,
}: ImportWorkflowButtonProps) {
const inputId = useId();
const credentialGetter = useCredentialGetter();
const queryClient = useQueryClient();
@@ -45,6 +49,10 @@ function ImportWorkflowButton({ onImportStart }: ImportWorkflowButtonProps) {
const createWorkflowFromYamlMutation = async (yaml: string) => {
try {
const client = await getClient(credentialGetter);
const params: Record<string, string> = {};
if (selectedFolderId) {
params.folder_id = selectedFolderId;
}
await client.post<string, { data: WorkflowApiResponse }>(
"/workflows",
yaml,
@@ -52,12 +60,16 @@ function ImportWorkflowButton({ onImportStart }: ImportWorkflowButtonProps) {
headers: {
"Content-Type": "text/plain",
},
params,
},
);
queryClient.invalidateQueries({
queryKey: ["workflows"],
});
queryClient.invalidateQueries({
queryKey: ["folders"],
});
toast({
variant: "success",
title: "Workflow imported",
@@ -78,10 +90,15 @@ function ImportWorkflowButton({ onImportStart }: ImportWorkflowButtonProps) {
formData.append("file", file);
const client = await getClient(credentialGetter);
const params: Record<string, string> = {};
if (selectedFolderId) {
params.folder_id = selectedFolderId;
}
await client.post("/workflows/import-pdf", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
params,
});
// Notify parent to start polling

View File

@@ -32,6 +32,7 @@ import {
LightningBoltIcon,
MagnifyingGlassIcon,
MixerHorizontalIcon,
Pencil2Icon,
PlayIcon,
PlusIcon,
ReloadIcon,
@@ -415,11 +416,17 @@ function Workflows() {
/>
</div>
<div className="flex gap-4">
<ImportWorkflowButton onImportStart={startPolling} />
<ImportWorkflowButton
onImportStart={startPolling}
selectedFolderId={selectedFolderId}
/>
<Button
disabled={createWorkflowMutation.isPending}
onClick={() => {
createWorkflowMutation.mutate(emptyWorkflowRequest);
createWorkflowMutation.mutate({
...emptyWorkflowRequest,
folder_id: selectedFolderId,
});
}}
>
{createWorkflowMutation.isPending ? (
@@ -591,12 +598,23 @@ function Workflows() {
</TableCell>
<TableCell>
<div className="flex justify-end gap-2">
<WorkflowFolderSelector
workflowPermanentId={
workflow.workflow_permanent_id
}
currentFolderId={workflow.folder_id}
/>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<WorkflowFolderSelector
workflowPermanentId={
workflow.workflow_permanent_id
}
currentFolderId={workflow.folder_id}
/>
</div>
</TooltipTrigger>
<TooltipContent>
Assign to Folder
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
@@ -625,6 +643,27 @@ function Workflows() {
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="outline"
onClick={(event) => {
handleIconClick(
event,
`/workflows/${workflow.workflow_permanent_id}/debug`,
);
}}
>
<Pencil2Icon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
Open in Editor
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
@@ -676,7 +715,7 @@ function Workflows() {
<div
key={idx}
className={cn(
"grid grid-cols-[140px_1fr_2fr] gap-4 rounded border bg-white p-3 text-sm dark:border-slate-800 dark:bg-slate-900",
"grid grid-cols-[minmax(200px,1fr)_minmax(200px,1fr)_minmax(300px,2fr)] gap-6 rounded border bg-white p-3 text-sm dark:border-slate-800 dark:bg-slate-900",
matchesParam &&
"shadow-[0_0_15px_rgba(59,130,246,0.3)] ring-2 ring-blue-500/50",
)}

View File

@@ -66,12 +66,15 @@ function WorkflowFolderSelector({
"h-8 w-8",
currentFolderId ? "text-blue-400" : "text-slate-400",
)}
title="Move to folder"
>
<FileIcon className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="end">
<PopoverContent
className="w-80 p-0"
align="end"
onCloseAutoFocus={(e) => e.preventDefault()}
>
<div className="border-b p-3">
<h4 className="mb-2 text-sm font-medium">Move to folder</h4>
<div className="relative">

View File

@@ -57,8 +57,9 @@ export function useActiveImportsPolling() {
description: `Successfully imported ${prevImport.title || "workflow"}`,
});
// Refresh workflows to show new workflow
// Refresh workflows and folders to show new workflow and update folder counts
queryClient.invalidateQueries({ queryKey: ["workflows"] });
queryClient.invalidateQueries({ queryKey: ["folders"] });
}
});

View File

@@ -29,6 +29,9 @@ function useCreateWorkflowMutation() {
queryClient.invalidateQueries({
queryKey: ["workflows"],
});
queryClient.invalidateQueries({
queryKey: ["folders"],
});
navigate(`/workflows/${response.data.workflow_permanent_id}/debug`);
},
});

View File

@@ -20,6 +20,7 @@ export type WorkflowCreateYAMLRequest = {
ai_fallback?: boolean;
run_sequentially?: boolean;
sequential_key?: string | null;
folder_id?: string | null;
};
export type WorkflowDefinitionYAML = {

View File

@@ -1436,6 +1436,23 @@ class AgentDB:
if version:
workflow.version = version
session.add(workflow)
# Update folder's modified_at if folder_id is provided
if folder_id:
# Validate folder exists and belongs to the same organization
folder_stmt = (
select(FolderModel)
.where(FolderModel.folder_id == folder_id)
.where(FolderModel.organization_id == organization_id)
.where(FolderModel.deleted_at.is_(None))
)
folder_model = await session.scalar(folder_stmt)
if not folder_model:
raise ValueError(
f"Folder {folder_id} not found or does not belong to organization {organization_id}"
)
folder_model.modified_at = datetime.utcnow()
await session.commit()
await session.refresh(workflow)
return convert_to_workflow(workflow, self.debug_enabled)

View File

@@ -484,6 +484,7 @@ async def cancel_run(
)
async def create_workflow_legacy(
request: Request,
folder_id: str | None = Query(None, description="Optional folder ID to assign the workflow to"),
current_org: Organization = Depends(org_auth_service.get_current_org),
) -> Workflow:
analytics.capture("skyvern-oss-agent-workflow-create-legacy")
@@ -495,6 +496,9 @@ async def create_workflow_legacy(
try:
workflow_create_request = WorkflowCreateYAMLRequest.model_validate(workflow_yaml)
# Override folder_id if provided as query parameter
if folder_id is not None:
workflow_create_request.folder_id = folder_id
return await app.WORKFLOW_SERVICE.create_workflow_from_request(
organization=current_org, request=workflow_create_request
)
@@ -535,6 +539,7 @@ async def create_workflow_legacy(
)
async def create_workflow(
data: WorkflowRequest,
folder_id: str | None = Query(None, description="Optional folder ID to assign the workflow to"),
current_org: Organization = Depends(org_auth_service.get_current_org),
) -> Workflow:
analytics.capture("skyvern-oss-agent-workflow-create")
@@ -549,6 +554,9 @@ async def create_workflow(
status_code=422,
detail="Invalid workflow definition. Workflow should be provided in either yaml or json format.",
)
# Override folder_id if provided as query parameter
if folder_id is not None:
workflow_definition.folder_id = folder_id
return await app.WORKFLOW_SERVICE.create_workflow_from_request(
organization=current_org,
request=workflow_definition,
@@ -669,6 +677,7 @@ async def _validate_file_size(file: UploadFile) -> UploadFile:
async def import_workflow_from_pdf(
background_tasks: BackgroundTasks,
file: UploadFile = Depends(_validate_file_size),
folder_id: str | None = Query(None, description="Optional folder ID to assign the imported workflow to"),
current_org: Organization = Depends(org_auth_service.get_current_org),
) -> dict[str, Any]:
"""Import a workflow from a PDF file containing Standard Operating Procedures."""
@@ -702,6 +711,7 @@ async def import_workflow_from_pdf(
workflow_definition={"parameters": [], "blocks": []},
organization_id=current_org.organization_id,
status=WorkflowStatus.importing,
folder_id=folder_id,
)
# Process PDF import in background (LLM call is the slow part)

View File

@@ -2771,6 +2771,7 @@ class WorkflowService:
ai_fallback=request.ai_fallback,
run_sequentially=request.run_sequentially,
sequential_key=request.sequential_key,
folder_id=request.folder_id,
)
# Keeping track of the new workflow id to delete it if an error occurs during the creation process
new_workflow_id = potential_workflow.workflow_id

View File

@@ -562,6 +562,7 @@ class WorkflowCreateYAMLRequest(BaseModel):
cache_key: str | None = "default"
run_sequentially: bool = False
sequential_key: str | None = None
folder_id: str | None = None
class WorkflowRequest(BaseModel):