improvements for folders and parameters (#3918)
Co-authored-by: Jonathan Dobson <jon.m.dobson@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"] });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ function useCreateWorkflowMutation() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["workflows"],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["folders"],
|
||||
});
|
||||
navigate(`/workflows/${response.data.workflow_permanent_id}/debug`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ export type WorkflowCreateYAMLRequest = {
|
||||
ai_fallback?: boolean;
|
||||
run_sequentially?: boolean;
|
||||
sequential_key?: string | null;
|
||||
folder_id?: string | null;
|
||||
};
|
||||
|
||||
export type WorkflowDefinitionYAML = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user