[Frontend] Saving Workflows as Templates (#4279)
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuPortal,
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuSeparator,
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
@@ -24,6 +25,8 @@ import {
|
|||||||
import { toast } from "@/components/ui/use-toast";
|
import { toast } from "@/components/ui/use-toast";
|
||||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
import {
|
import {
|
||||||
|
BookmarkFilledIcon,
|
||||||
|
BookmarkIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
DotsHorizontalIcon,
|
DotsHorizontalIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
@@ -98,6 +101,46 @@ function WorkflowActions({ workflow, onSuccessfullyDeleted }: Props) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const templateMutation = useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
workflowPermanentId,
|
||||||
|
isTemplate,
|
||||||
|
}: {
|
||||||
|
workflowPermanentId: string;
|
||||||
|
isTemplate: boolean;
|
||||||
|
}) => {
|
||||||
|
// Template endpoint only exists on /v1 (no /api prefix)
|
||||||
|
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||||
|
return client.put(
|
||||||
|
`/workflows/${workflowPermanentId}/template?is_template=${isTemplate}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["workflows"],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["orgTemplates"],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["workflow", variables.workflowPermanentId],
|
||||||
|
});
|
||||||
|
toast({
|
||||||
|
title: variables.isTemplate
|
||||||
|
? "Saved as template"
|
||||||
|
: "Removed from templates",
|
||||||
|
variant: "success",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error: AxiosError) => {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Failed to update template status",
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -123,6 +166,26 @@ function WorkflowActions({ workflow, onSuccessfullyDeleted }: Props) {
|
|||||||
<CopyIcon className="mr-2 h-4 w-4" />
|
<CopyIcon className="mr-2 h-4 w-4" />
|
||||||
Clone Workflow
|
Clone Workflow
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => {
|
||||||
|
templateMutation.mutate({
|
||||||
|
workflowPermanentId: workflow.workflow_permanent_id,
|
||||||
|
isTemplate: !workflow.is_template,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="p-2"
|
||||||
|
disabled={templateMutation.isPending}
|
||||||
|
>
|
||||||
|
{workflow.is_template ? (
|
||||||
|
<BookmarkFilledIcon className="mr-2 h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<BookmarkIcon className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{workflow.is_template
|
||||||
|
? "Remove from Templates"
|
||||||
|
: "Save as Template"}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger>
|
<DropdownMenuSubTrigger>
|
||||||
<DownloadIcon className="mr-2 h-4 w-4" />
|
<DownloadIcon className="mr-2 h-4 w-4" />
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
|||||||
import { basicLocalTimeFormat, basicTimeFormat } from "@/util/timeFormat";
|
import { basicLocalTimeFormat, basicTimeFormat } from "@/util/timeFormat";
|
||||||
import { cn } from "@/util/utils";
|
import { cn } from "@/util/utils";
|
||||||
import {
|
import {
|
||||||
|
BookmarkFilledIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
DotsHorizontalIcon,
|
DotsHorizontalIcon,
|
||||||
FileIcon,
|
FileIcon,
|
||||||
LightningBoltIcon,
|
LightningBoltIcon,
|
||||||
@@ -39,9 +41,16 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { NarrativeCard } from "./components/header/NarrativeCard";
|
import { NarrativeCard } from "./components/header/NarrativeCard";
|
||||||
import { FolderCard } from "./components/FolderCard";
|
import { FolderCard } from "./components/FolderCard";
|
||||||
import { CreateFolderDialog } from "./components/CreateFolderDialog";
|
import { CreateFolderDialog } from "./components/CreateFolderDialog";
|
||||||
|
import { CreateFromTemplateDialog } from "./components/CreateFromTemplateDialog";
|
||||||
import { ViewAllFoldersDialog } from "./components/ViewAllFoldersDialog";
|
import { ViewAllFoldersDialog } from "./components/ViewAllFoldersDialog";
|
||||||
import { WorkflowFolderSelector } from "./components/WorkflowFolderSelector";
|
import { WorkflowFolderSelector } from "./components/WorkflowFolderSelector";
|
||||||
import { HighlightText } from "./components/HighlightText";
|
import { HighlightText } from "./components/HighlightText";
|
||||||
@@ -49,6 +58,7 @@ import { useCreateWorkflowMutation } from "./hooks/useCreateWorkflowMutation";
|
|||||||
import { useFoldersQuery } from "./hooks/useFoldersQuery";
|
import { useFoldersQuery } from "./hooks/useFoldersQuery";
|
||||||
import { useActiveImportsPolling } from "./hooks/useActiveImportsPolling";
|
import { useActiveImportsPolling } from "./hooks/useActiveImportsPolling";
|
||||||
import { ImportWorkflowButton } from "./ImportWorkflowButton";
|
import { ImportWorkflowButton } from "./ImportWorkflowButton";
|
||||||
|
import { convert } from "./editor/workflowEditorUtils";
|
||||||
import { WorkflowApiResponse } from "./types/workflowTypes";
|
import { WorkflowApiResponse } from "./types/workflowTypes";
|
||||||
import { WorkflowCreateYAMLRequest } from "./types/workflowYamlTypes";
|
import { WorkflowCreateYAMLRequest } from "./types/workflowYamlTypes";
|
||||||
import { WorkflowActions } from "./WorkflowActions";
|
import { WorkflowActions } from "./WorkflowActions";
|
||||||
@@ -88,6 +98,9 @@ function Workflows() {
|
|||||||
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
|
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
|
||||||
const [isViewAllFoldersOpen, setIsViewAllFoldersOpen] = useState(false);
|
const [isViewAllFoldersOpen, setIsViewAllFoldersOpen] = useState(false);
|
||||||
|
|
||||||
|
// Template dialog state
|
||||||
|
const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false);
|
||||||
|
|
||||||
// Poll for active imports
|
// Poll for active imports
|
||||||
const { activeImports, startPolling } = useActiveImportsPolling();
|
const { activeImports, startPolling } = useActiveImportsPolling();
|
||||||
|
|
||||||
@@ -390,22 +403,38 @@ function Workflows() {
|
|||||||
onImportStart={startPolling}
|
onImportStart={startPolling}
|
||||||
selectedFolderId={selectedFolderId}
|
selectedFolderId={selectedFolderId}
|
||||||
/>
|
/>
|
||||||
<Button
|
<DropdownMenu modal={false}>
|
||||||
disabled={createWorkflowMutation.isPending}
|
<DropdownMenuTrigger asChild>
|
||||||
onClick={() => {
|
<Button disabled={createWorkflowMutation.isPending}>
|
||||||
createWorkflowMutation.mutate({
|
{createWorkflowMutation.isPending ? (
|
||||||
...emptyWorkflowRequest,
|
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||||
folder_id: selectedFolderId,
|
) : (
|
||||||
});
|
<PlusIcon className="mr-2 h-4 w-4" />
|
||||||
}}
|
)}
|
||||||
>
|
Create
|
||||||
{createWorkflowMutation.isPending ? (
|
<ChevronDownIcon className="ml-2 h-4 w-4" />
|
||||||
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
</Button>
|
||||||
) : (
|
</DropdownMenuTrigger>
|
||||||
<PlusIcon className="mr-2 h-4 w-4" />
|
<DropdownMenuContent align="end">
|
||||||
)}
|
<DropdownMenuItem
|
||||||
Create
|
onSelect={() => {
|
||||||
</Button>
|
createWorkflowMutation.mutate({
|
||||||
|
...emptyWorkflowRequest,
|
||||||
|
folder_id: selectedFolderId,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon className="mr-2 h-4 w-4" />
|
||||||
|
Blank Workflow
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => setIsTemplateDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<BookmarkFilledIcon className="mr-2 h-4 w-4" />
|
||||||
|
From Template
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border">
|
<div className="rounded-lg border">
|
||||||
@@ -532,10 +561,22 @@ function Workflows() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<HighlightText
|
<div className="flex items-center gap-2">
|
||||||
text={workflow.title}
|
<HighlightText
|
||||||
query={debouncedSearch}
|
text={workflow.title}
|
||||||
/>
|
query={debouncedSearch}
|
||||||
|
/>
|
||||||
|
{workflow.is_template && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<BookmarkFilledIcon className="h-3.5 w-3.5 shrink-0 text-blue-500" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Template</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
@@ -749,6 +790,22 @@ function Workflows() {
|
|||||||
onFolderSelect={setSelectedFolderId}
|
onFolderSelect={setSelectedFolderId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Template Dialog */}
|
||||||
|
<CreateFromTemplateDialog
|
||||||
|
open={isTemplateDialogOpen}
|
||||||
|
onOpenChange={setIsTemplateDialogOpen}
|
||||||
|
onSelectTemplate={(template) => {
|
||||||
|
const clonedWorkflow = convert({
|
||||||
|
...template,
|
||||||
|
title: `${template.title} (copy)`,
|
||||||
|
});
|
||||||
|
createWorkflowMutation.mutate({
|
||||||
|
...clonedWorkflow,
|
||||||
|
folder_id: selectedFolderId,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<WorkflowTemplates />
|
<WorkflowTemplates />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { BookmarkFilledIcon, MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||||
|
import { useOrgTemplatesQuery } from "../hooks/useOrgTemplatesQuery";
|
||||||
|
import { WorkflowApiResponse } from "../types/workflowTypes";
|
||||||
|
import { cn } from "@/util/utils";
|
||||||
|
|
||||||
|
interface CreateFromTemplateDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSelectTemplate: (template: WorkflowApiResponse) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateFromTemplateDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSelectTemplate,
|
||||||
|
}: CreateFromTemplateDialogProps) {
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const { data: templates = [], isLoading } = useOrgTemplatesQuery();
|
||||||
|
|
||||||
|
const filteredTemplates = templates.filter((template) =>
|
||||||
|
template.title.toLowerCase().includes(search.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelect = (template: WorkflowApiResponse) => {
|
||||||
|
onSelectTemplate(template);
|
||||||
|
onOpenChange(false);
|
||||||
|
setSearch("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
onOpenChange(open);
|
||||||
|
if (!open) {
|
||||||
|
setSearch("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create from Template</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Select a template to create a new workflow with pre-filled blocks.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="relative">
|
||||||
|
<MagnifyingGlassIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
|
||||||
|
<Input
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
placeholder="Search templates..."
|
||||||
|
className="pl-9"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-[400px] overflow-y-auto">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-24 rounded-lg" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : filteredTemplates.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<BookmarkFilledIcon className="mb-3 h-10 w-10 text-slate-400" />
|
||||||
|
{templates.length === 0 ? (
|
||||||
|
<>
|
||||||
|
<p className="text-slate-600 dark:text-slate-300">
|
||||||
|
No templates yet
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
Save a workflow as a template to see it here.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-slate-600 dark:text-slate-300">
|
||||||
|
No templates match your search
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
Try a different search term.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{filteredTemplates.map((template) => (
|
||||||
|
<Button
|
||||||
|
key={template.workflow_permanent_id}
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"flex h-auto flex-col items-start gap-1 p-4 text-left",
|
||||||
|
"hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-950/30",
|
||||||
|
)}
|
||||||
|
onClick={() => handleSelect(template)}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center gap-2">
|
||||||
|
<BookmarkFilledIcon className="h-4 w-4 shrink-0 text-blue-500" />
|
||||||
|
<span className="truncate font-medium">
|
||||||
|
{template.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{template.description && (
|
||||||
|
<p className="line-clamp-2 text-xs text-slate-500">
|
||||||
|
{template.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-slate-400">
|
||||||
|
{template.workflow_definition.blocks.length} block
|
||||||
|
{template.workflow_definition.blocks.length !== 1
|
||||||
|
? "s"
|
||||||
|
: ""}
|
||||||
|
</p>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { CreateFromTemplateDialog };
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
|
BookmarkFilledIcon,
|
||||||
|
BookmarkIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ChevronUpIcon,
|
ChevronUpIcon,
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
@@ -21,6 +23,11 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||||
import { useGlobalWorkflowsQuery } from "../hooks/useGlobalWorkflowsQuery";
|
import { useGlobalWorkflowsQuery } from "../hooks/useGlobalWorkflowsQuery";
|
||||||
|
import { getClient } from "@/api/AxiosClient";
|
||||||
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import { toast } from "@/components/ui/use-toast";
|
||||||
import { EditableNodeTitle } from "./nodes/components/EditableNodeTitle";
|
import { EditableNodeTitle } from "./nodes/components/EditableNodeTitle";
|
||||||
import { useCreateWorkflowMutation } from "../hooks/useCreateWorkflowMutation";
|
import { useCreateWorkflowMutation } from "../hooks/useCreateWorkflowMutation";
|
||||||
import { convert } from "./workflowEditorUtils";
|
import { convert } from "./workflowEditorUtils";
|
||||||
@@ -41,6 +48,7 @@ type Props = {
|
|||||||
cacheKeyValues: CacheKeyValuesResponse | undefined;
|
cacheKeyValues: CacheKeyValuesResponse | undefined;
|
||||||
cacheKeyValuesPanelOpen: boolean;
|
cacheKeyValuesPanelOpen: boolean;
|
||||||
isGeneratingCode?: boolean;
|
isGeneratingCode?: boolean;
|
||||||
|
isTemplate?: boolean;
|
||||||
parametersPanelOpen: boolean;
|
parametersPanelOpen: boolean;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
showAllCode: boolean;
|
showAllCode: boolean;
|
||||||
@@ -61,6 +69,7 @@ function WorkflowHeader({
|
|||||||
cacheKeyValues,
|
cacheKeyValues,
|
||||||
cacheKeyValuesPanelOpen,
|
cacheKeyValuesPanelOpen,
|
||||||
isGeneratingCode,
|
isGeneratingCode,
|
||||||
|
isTemplate,
|
||||||
parametersPanelOpen,
|
parametersPanelOpen,
|
||||||
saving,
|
saving,
|
||||||
showAllCode,
|
showAllCode,
|
||||||
@@ -90,6 +99,41 @@ function WorkflowHeader({
|
|||||||
cacheKeyValue ?? null,
|
cacheKeyValue ?? null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const credentialGetter = useCredentialGetter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const templateMutation = useMutation({
|
||||||
|
mutationFn: async (newIsTemplate: boolean) => {
|
||||||
|
// Template endpoint only exists on /v1 (no /api prefix)
|
||||||
|
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||||
|
return client.put(
|
||||||
|
`/workflows/${workflowPermanentId}/template?is_template=${newIsTemplate}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSuccess: (_, newIsTemplate) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["workflows"],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["orgTemplates"],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["workflow", workflowPermanentId],
|
||||||
|
});
|
||||||
|
toast({
|
||||||
|
title: newIsTemplate ? "Saved as template" : "Removed from templates",
|
||||||
|
variant: "success",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error: AxiosError) => {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Failed to update template status",
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const dom: Dom = {
|
const dom: Dom = {
|
||||||
input: useRef<HTMLInputElement>(null),
|
input: useRef<HTMLInputElement>(null),
|
||||||
};
|
};
|
||||||
@@ -296,6 +340,39 @@ function WorkflowHeader({
|
|||||||
<TooltipContent>Save</TooltipContent>
|
<TooltipContent>Save</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
disabled={
|
||||||
|
isRecording || templateMutation.isPending || saving
|
||||||
|
}
|
||||||
|
size="icon"
|
||||||
|
variant={isTemplate ? "default" : "tertiary"}
|
||||||
|
className="size-10 min-w-[2.5rem]"
|
||||||
|
onClick={() => {
|
||||||
|
const newIsTemplate = !isTemplate;
|
||||||
|
if (newIsTemplate) {
|
||||||
|
// When saving AS template, save the workflow first
|
||||||
|
onSave();
|
||||||
|
}
|
||||||
|
templateMutation.mutate(newIsTemplate);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{templateMutation.isPending ? (
|
||||||
|
<ReloadIcon className="size-6 animate-spin" />
|
||||||
|
) : isTemplate ? (
|
||||||
|
<BookmarkFilledIcon className="size-6" />
|
||||||
|
) : (
|
||||||
|
<BookmarkIcon className="size-6" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{isTemplate ? "Remove from Templates" : "Save as Template"}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
{!workflowRunIsRunningOrQueued && (
|
{!workflowRunIsRunningOrQueued && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|||||||
@@ -1123,6 +1123,7 @@ function Workspace({
|
|||||||
cacheKeyValue={cacheKeyValue}
|
cacheKeyValue={cacheKeyValue}
|
||||||
cacheKeyValues={cacheKeyValues}
|
cacheKeyValues={cacheKeyValues}
|
||||||
isGeneratingCode={isGeneratingCode}
|
isGeneratingCode={isGeneratingCode}
|
||||||
|
isTemplate={workflow?.is_template}
|
||||||
saving={workflowChangesStore.saveIsPending}
|
saving={workflowChangesStore.saveIsPending}
|
||||||
cacheKeyValuesPanelOpen={
|
cacheKeyValuesPanelOpen={
|
||||||
workflowPanelState.active &&
|
workflowPanelState.active &&
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getClient } from "@/api/AxiosClient";
|
||||||
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
|
import { WorkflowApiResponse } from "../types/workflowTypes";
|
||||||
|
|
||||||
|
function useOrgTemplatesQuery() {
|
||||||
|
const credentialGetter = useCredentialGetter();
|
||||||
|
|
||||||
|
return useQuery<WorkflowApiResponse[]>({
|
||||||
|
queryKey: ["orgTemplates"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const client = await getClient(credentialGetter);
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("only_templates", "true");
|
||||||
|
params.append("page_size", "100");
|
||||||
|
return client
|
||||||
|
.get<WorkflowApiResponse[]>("/workflows", { params })
|
||||||
|
.then((response) => response.data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useOrgTemplatesQuery };
|
||||||
@@ -562,6 +562,7 @@ export type WorkflowApiResponse = {
|
|||||||
workflow_id: string;
|
workflow_id: string;
|
||||||
organization_id: string;
|
organization_id: string;
|
||||||
is_saved_task: boolean;
|
is_saved_task: boolean;
|
||||||
|
is_template: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
workflow_permanent_id: string;
|
workflow_permanent_id: string;
|
||||||
version: number;
|
version: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user