Skip to content

Commit e164b72

Browse files
committed
feat: add template detail modal and move page title to navbar
Add click-to-preview modal on template cards showing large graph, full title/description, and Use Template CTA. Move page title to AppTopBar navbar and remove in-page header to match Schedules layout. Signed-off-by: Krishna Mohan <krishanmohank974@gmail.com>
1 parent 59a7bb5 commit e164b72

2 files changed

Lines changed: 167 additions & 46 deletions

File tree

frontend/src/components/layout/AppTopBar.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ export function AppTopBar({
4545
};
4646
}
4747

48+
if (location.pathname === '/templates') {
49+
return {
50+
title: 'Template Library',
51+
shortTitle: 'Templates',
52+
subtitle: 'Browse and use pre-built workflow templates',
53+
};
54+
}
55+
4856
if (location.pathname.startsWith('/schedules')) {
4957
return {
5058
title: 'Workflow Schedules',

frontend/src/pages/TemplateLibraryPage.tsx

Lines changed: 159 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { Skeleton } from '@/components/ui/skeleton';
1414
import {
1515
Eye,
1616
Filter,
17-
Package,
1817
RefreshCw,
1918
Search,
2019
Star,
@@ -29,7 +28,6 @@ import {
2928
Database,
3029
Link,
3130
TestTube2,
32-
FileText,
3331
MoreHorizontal,
3432
AlertTriangle,
3533
Layers,
@@ -53,6 +51,13 @@ import { track, Events } from '@/features/analytics/events';
5351
import { UseTemplateModal } from '@/features/templates/UseTemplateModal';
5452
import { WorkflowPreview } from '@/features/templates/WorkflowPreview';
5553
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
54+
import {
55+
Dialog,
56+
DialogContent,
57+
DialogHeader,
58+
DialogTitle,
59+
DialogDescription,
60+
} from '@/components/ui/dialog';
5661
import { cn } from '@/lib/utils';
5762

5863
// ---------------------------------------------------------------------------
@@ -344,24 +349,26 @@ function PreviewSection({ graph }: { graph?: Record<string, unknown>; category?:
344349
interface TemplateCardProps {
345350
template: Template;
346351
onUse: (template: Template) => void;
352+
onPreview: (template: Template) => void;
347353
canUse: boolean;
348354
}
349355

350-
function TemplateCard({ template, onUse, canUse }: TemplateCardProps) {
356+
function TemplateCard({ template, onUse, onPreview, canUse }: TemplateCardProps) {
351357
const catStyle = getCategoryStyle(template.category);
352358
const CategoryIcon = catStyle.icon;
353359

354360
return (
355361
<div
356362
className={cn(
357-
'group flex flex-col rounded-2xl',
363+
'group flex flex-col rounded-2xl cursor-pointer',
358364
'bg-white dark:bg-zinc-900',
359365
'border border-gray-100 dark:border-white/5',
360366
'shadow-sm',
361367
'transition-all duration-300 ease-out',
362368
'hover:shadow-lg hover:-translate-y-1',
363369
'dark:hover:border-white/10',
364370
)}
371+
onClick={() => onPreview(template)}
365372
>
366373
{/* Content wrapper with padding */}
367374
<div className="flex flex-col flex-1 p-5 md:p-6 gap-6">
@@ -492,7 +499,10 @@ function TemplateCard({ template, onUse, canUse }: TemplateCardProps) {
492499
'flex-1 h-11 rounded-xl font-medium gap-2',
493500
'active:scale-[0.98] transition-all duration-200',
494501
)}
495-
onClick={() => onUse(template)}
502+
onClick={(e) => {
503+
e.stopPropagation();
504+
onUse(template);
505+
}}
496506
disabled={!canUse}
497507
>
498508
Use Template
@@ -516,6 +526,7 @@ function TemplateCard({ template, onUse, canUse }: TemplateCardProps) {
516526
href={`https://github.com/${template.repository}/blob/${template.branch || 'main'}/${template.path}`}
517527
target="_blank"
518528
rel="noopener noreferrer"
529+
onClick={(e) => e.stopPropagation()}
519530
>
520531
<Eye className="h-4 w-4" />
521532
</a>
@@ -594,6 +605,121 @@ function EmptyState({ hasFilters }: { hasFilters: boolean }) {
594605
);
595606
}
596607

608+
// ---------------------------------------------------------------------------
609+
// Template detail modal
610+
// ---------------------------------------------------------------------------
611+
612+
interface TemplateDetailModalProps {
613+
template: Template | null;
614+
open: boolean;
615+
onOpenChange: (open: boolean) => void;
616+
onUse: (template: Template) => void;
617+
canUse: boolean;
618+
}
619+
620+
function TemplateDetailModal({
621+
template,
622+
open,
623+
onOpenChange,
624+
onUse,
625+
canUse,
626+
}: TemplateDetailModalProps) {
627+
if (!template) return null;
628+
629+
const catStyle = getCategoryStyle(template.category);
630+
const CategoryIcon = catStyle.icon;
631+
const hasGraph =
632+
template.graph &&
633+
(template.graph as any)?.nodes &&
634+
Array.isArray((template.graph as any).nodes) &&
635+
(template.graph as any).nodes.length > 0;
636+
637+
return (
638+
<Dialog open={open} onOpenChange={onOpenChange}>
639+
<DialogContent className="max-w-3xl max-h-[85vh] overflow-y-auto p-0">
640+
{/* Graph preview */}
641+
<div
642+
className="relative w-full h-72 sm:h-80 overflow-hidden rounded-t-lg"
643+
style={{
644+
background: 'linear-gradient(180deg, #F8FAFF 0%, #F1F5FF 100%)',
645+
}}
646+
>
647+
<div
648+
className="absolute inset-0 hidden dark:block"
649+
style={{
650+
background: 'linear-gradient(180deg, #111827 0%, #0B1220 100%)',
651+
}}
652+
/>
653+
<div
654+
className="absolute inset-0 hidden dark:block pointer-events-none"
655+
style={{
656+
background:
657+
'radial-gradient(circle at 50% 0%, rgba(99,102,241,0.08), transparent 60%)',
658+
}}
659+
/>
660+
<div
661+
className="absolute inset-0 opacity-[0.03] pointer-events-none"
662+
style={{
663+
backgroundImage:
664+
'radial-gradient(circle, hsl(var(--foreground)) 0.5px, transparent 0.5px)',
665+
backgroundSize: '12px 12px',
666+
}}
667+
/>
668+
{hasGraph ? (
669+
<div className="absolute inset-0 flex items-center justify-center p-4">
670+
<WorkflowPreview graph={template.graph!} className="w-full h-full" />
671+
</div>
672+
) : (
673+
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2 text-muted-foreground/30">
674+
<Workflow className="h-12 w-12" />
675+
<span className="text-xs font-medium">No preview</span>
676+
</div>
677+
)}
678+
</div>
679+
680+
{/* Content */}
681+
<div className="px-6 pb-6 space-y-4">
682+
<DialogHeader>
683+
<div className="flex items-center gap-2 mb-2">
684+
<Badge
685+
variant="outline"
686+
className={cn(
687+
'text-xs font-medium gap-1 rounded-full px-3 py-1 border',
688+
catStyle.badge,
689+
)}
690+
>
691+
<CategoryIcon className="h-3 w-3" />
692+
{template.category || 'Automation'}
693+
</Badge>
694+
{template.author && (
695+
<span className="text-xs text-muted-foreground">by {template.author}</span>
696+
)}
697+
</div>
698+
<DialogTitle className="text-2xl font-semibold">
699+
{toTitleCase(template.name)}
700+
</DialogTitle>
701+
{template.description && (
702+
<DialogDescription className="text-sm mt-2">{template.description}</DialogDescription>
703+
)}
704+
</DialogHeader>
705+
706+
<Button
707+
className={cn(
708+
'w-full h-11 rounded-xl font-medium gap-2',
709+
'active:scale-[0.98] transition-all duration-200',
710+
)}
711+
onClick={() => onUse(template)}
712+
disabled={!canUse}
713+
>
714+
Use Template
715+
<ArrowRight className="h-4 w-4" />
716+
</Button>
717+
</div>
718+
</DialogContent>
719+
</Dialog>
720+
);
721+
}
722+
597723
// ---------------------------------------------------------------------------
598724
// Main page
599725
// ---------------------------------------------------------------------------
@@ -625,6 +751,7 @@ export function TemplateLibraryPage() {
625751

626752
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
627753
const [isUseModalOpen, setIsUseModalOpen] = useState(false);
754+
const [previewTemplate, setPreviewTemplate] = useState<Template | null>(null);
628755

629756
const handleSync = () => {
630757
syncMutation.mutate();
@@ -673,49 +800,9 @@ export function TemplateLibraryPage() {
673800
return (
674801
<div className="flex-1 bg-background">
675802
<div className="container mx-auto py-6 md:py-8 px-3 md:px-6 max-w-7xl">
676-
{/* Header */}
677-
<div className="mb-8">
678-
<div className="flex items-start justify-between gap-4">
679-
<div>
680-
<div className="flex items-center gap-3 mb-1.5">
681-
<div className="h-9 w-9 rounded-lg bg-primary/10 flex items-center justify-center">
682-
<Package className="h-5 w-5 text-primary" />
683-
</div>
684-
<div>
685-
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">
686-
Template Library
687-
</h1>
688-
</div>
689-
</div>
690-
<p className="text-muted-foreground text-sm ml-12">
691-
Browse and use pre-built workflow templates to accelerate your automation
692-
</p>
693-
</div>
694-
695-
<div className="flex items-center gap-2 flex-shrink-0">
696-
{!isLoading && templates.length > 0 && (
697-
<Badge variant="secondary" className="text-xs font-medium gap-1 hidden sm:flex">
698-
<FileText className="h-3 w-3" />
699-
{templates.length} template{templates.length !== 1 ? 's' : ''}
700-
</Badge>
701-
)}
702-
<Button
703-
variant="outline"
704-
size="sm"
705-
onClick={handleSync}
706-
disabled={isSyncing || !canManageWorkflows}
707-
className="gap-2 h-8"
708-
>
709-
<RefreshCw className={cn('h-3.5 w-3.5', isSyncing && 'animate-spin')} />
710-
<span className="hidden sm:inline">Sync</span>
711-
</Button>
712-
</div>
713-
</div>
714-
</div>
715-
716803
{/* Filters */}
717804
<div className="mb-6 space-y-3">
718-
{/* Search + Category */}
805+
{/* Search + Category + Sync */}
719806
<div className="flex gap-3">
720807
<div className="relative flex-1">
721808
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
@@ -751,6 +838,17 @@ export function TemplateLibraryPage() {
751838
})}
752839
</SelectContent>
753840
</Select>
841+
842+
<Button
843+
variant="outline"
844+
size="sm"
845+
onClick={handleSync}
846+
disabled={isSyncing || !canManageWorkflows}
847+
className="gap-2 h-9"
848+
>
849+
<RefreshCw className={cn('h-3.5 w-3.5', isSyncing && 'animate-spin')} />
850+
<span className="hidden sm:inline">Sync</span>
851+
</Button>
754852
</div>
755853

756854
{/* Tags + Clear */}
@@ -819,13 +917,28 @@ export function TemplateLibraryPage() {
819917
key={template.id}
820918
template={template}
821919
onUse={handleUseTemplate}
920+
onPreview={setPreviewTemplate}
822921
canUse={canManageWorkflows}
823922
/>
824923
))}
825924
</div>
826925
)}
827926
</div>
828927

928+
{/* Template Detail Modal */}
929+
<TemplateDetailModal
930+
template={previewTemplate}
931+
open={!!previewTemplate}
932+
onOpenChange={(open) => {
933+
if (!open) setPreviewTemplate(null);
934+
}}
935+
onUse={(template) => {
936+
setPreviewTemplate(null);
937+
handleUseTemplate(template);
938+
}}
939+
canUse={canManageWorkflows}
940+
/>
941+
829942
{/* Use Template Modal */}
830943
{selectedTemplate && (
831944
<UseTemplateModal

0 commit comments

Comments
 (0)