select-flow-template-dialog.tsx•9.12 kB
import { DialogDescription } from '@radix-ui/react-dialog';
import { useMutation, useQuery } from '@tanstack/react-query';
import { t } from 'i18next';
import { ArrowLeft, Info, Search, SearchX } from 'lucide-react';
import React, { useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { InputWithIcon } from '@/components/custom/input-with-icon';
import { ApMarkdown } from '@/components/custom/markdown';
import { Button } from '@/components/ui/button';
import {
Carousel,
CarouselApi,
CarouselContent,
CarouselItem,
} from '@/components/ui/carousel';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { ScrollArea } from '@/components/ui/scroll-area';
import { LoadingSpinner } from '@/components/ui/spinner';
import {
TooltipContent,
TooltipTrigger,
Tooltip,
} from '@/components/ui/tooltip';
import { PieceIconList } from '@/features/pieces/components/piece-icon-list';
import { templatesApi } from '@/features/templates/lib/templates-api';
import { authenticationSession } from '@/lib/authentication-session';
import {
MarkdownVariant,
FlowOperationType,
FlowTemplate,
PopulatedFlow,
} from '@activepieces/shared';
import { flowsApi } from '../lib/flows-api';
type TemplateCardProps = {
template: FlowTemplate;
onSelectTemplate: (template: FlowTemplate) => void;
};
const TemplateCard = ({ template, onSelectTemplate }: TemplateCardProps) => {
const selectTemplate = (template: FlowTemplate) => {
onSelectTemplate(template);
};
const navigate = useNavigate();
const { mutate: createFlow, isPending } = useMutation<
PopulatedFlow,
Error,
FlowTemplate
>({
mutationFn: async (template: FlowTemplate) => {
const newFlow = await flowsApi.create({
displayName: template.name,
projectId: authenticationSession.getProjectId()!,
});
return await flowsApi.update(newFlow.id, {
type: FlowOperationType.IMPORT_FLOW,
request: {
displayName: template.name,
trigger: template.template.trigger,
schemaVersion: template.template.schemaVersion,
},
});
},
onSuccess: (flow) => {
navigate(`/flows/${flow.id}`);
},
});
return (
<div
key={template.id}
className="rounded-lg border border-solid border-dividers overflow-hidden"
>
<div className="flex items-center gap-2 p-4 ">
<PieceIconList
trigger={template.template.trigger}
maxNumberOfIconsToShow={2}
/>
</div>
<div className="text-sm font-medium px-4 min-h-16">{template.name}</div>
<div className="py-2 px-4 gap-1 flex items-center">
<Button
variant="default"
loading={isPending}
className="px-2 h-8"
onClick={() => createFlow(template)}
>
{t('Use Template')}
</Button>
<Tooltip>
<TooltipTrigger asChild>
<div className="size-10 flex justify-center items-center">
<Button
variant="ghost"
className="rounded-full p-3 hover:bg-muted cursor-pointer flex justify-center items-center"
onClick={() => selectTemplate(template)}
>
<Info className="w-4 h-4" />
</Button>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
<span className="text-sm">{t('Learn more')}</span>
</TooltipContent>
</Tooltip>
</div>
</div>
);
};
const SelectFlowTemplateDialog = ({
children,
}: {
children: React.ReactNode;
}) => {
const [search, setSearch] = useState<string>('');
const carousel = useRef<CarouselApi>();
const [selectedTemplate, setSelectedTemplate] = useState<FlowTemplate | null>(
null,
);
const { data: templates, isLoading } = useQuery<FlowTemplate[], Error>({
queryKey: ['templates'],
queryFn: async () => {
const templates = await templatesApi.list();
return templates.data;
},
staleTime: 0,
});
const filteredTemplates = templates?.filter((template) => {
const templateName = template.name.toLowerCase();
const templateDescription = template.description.toLowerCase();
return (
templateName.includes(search.toLowerCase()) ||
templateDescription.includes(search.toLowerCase())
);
});
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearch(event.target.value);
};
const unselectTemplate = () => {
setSelectedTemplate(null);
carousel.current?.scrollPrev();
};
return (
<Dialog onOpenChange={unselectTemplate}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className=" lg:min-w-[850px] flex flex-col">
<DialogHeader>
<DialogTitle className="flex min-h-9 flex-row items-center justify-start gap-2 items-center h-full">
{selectedTemplate && (
<Button variant="ghost" size="sm" onClick={unselectTemplate}>
<ArrowLeft className="w-4 h-4" />
</Button>
)}
{t('Browse Templates')}
</DialogTitle>
</DialogHeader>
<Carousel setApi={(api) => (carousel.current = api)}>
<CarouselContent className="min-h-[300px] h-[70vh] max-h-[820px] ">
<CarouselItem key="templates">
<div>
<div className="p-1 ">
<InputWithIcon
icon={<Search className="w-4 h-4" />}
type="text"
value={search}
onChange={handleSearchChange}
placeholder={t('Search templates')}
className="mb-4"
/>
</div>
<DialogDescription>
{isLoading ? (
<div className="min-h-[300px] h-[70vh] max-h-[680px] o flex justify-center items-center">
<LoadingSpinner />
</div>
) : (
<>
{filteredTemplates?.length === 0 && (
<div className="flex flex-col items-center justify-center gap-2 text-center ">
<SearchX className="w-10 h-10" />
{t('No templates found, try adjusting your search')}
</div>
)}
<ScrollArea className="min-h-[260px] h-[calc(70vh-80px)] max-h-[740px] overflow-y-auto px-1">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredTemplates?.map((template) => (
<TemplateCard
key={template.id}
template={template}
onSelectTemplate={(template) => {
setSelectedTemplate(template);
carousel.current?.scrollNext();
}}
/>
))}
</div>
</ScrollArea>
</>
)}
</DialogDescription>
</div>
</CarouselItem>
<CarouselItem key="template-details">
{selectedTemplate && (
<div className="px-2 ">
<div className="mb-4 p-8 flex items-center justify-center gap-2 width-full bg-green-300 rounded-lg">
<PieceIconList
size="xxl"
trigger={selectedTemplate.template.trigger}
maxNumberOfIconsToShow={3}
/>
</div>
<ScrollArea className="px-2 min-h-[156px] h-[calc(70vh-144px)] max-h-[536px]">
<div className="mb-4 text-lg font-medium font-black">
{selectedTemplate?.name}
</div>
<ApMarkdown
markdown={selectedTemplate?.description}
variant={MarkdownVariant.BORDERLESS}
/>
{selectedTemplate.blogUrl && (
<div className="mt-4">
{t('Read more about this template in')}{' '}
<a
href={selectedTemplate.blogUrl}
target="_blank"
className="text-primary underline underline-offset-4"
rel="noreferrer"
>
{t('this blog!')}
</a>
</div>
)}
</ScrollArea>
</div>
)}
</CarouselItem>
</CarouselContent>
</Carousel>
</DialogContent>
</Dialog>
);
};
export { SelectFlowTemplateDialog };