diff --git a/frontend/src/components/script-editor/ScriptTemplateListView.tsx b/frontend/src/components/script-editor/ScriptTemplateListView.tsx new file mode 100644 index 00000000..6774fcd0 --- /dev/null +++ b/frontend/src/components/script-editor/ScriptTemplateListView.tsx @@ -0,0 +1,207 @@ +import { useState, useEffect } from 'react' +import { Plus, Search, Pencil, Trash2, Users, User as UserIcon, Loader2, FileCode } from 'lucide-react' +import { cn } from '@/lib/utils' +import { usePermissions } from '@/hooks/usePermissions' +import { scriptsApi } from '@/api' +import type { ScriptTemplateListItem, ScriptCategoryResponse } from '@/types' + +const COMPLEXITY_CLASSES = { + beginner: 'text-emerald-400 bg-emerald-400/10', + intermediate: 'text-amber-400 bg-amber-400/10', + advanced: 'text-rose-500 bg-rose-500/10', +} as const + +interface Props { + onEdit: (id: string) => void + onCreate: () => void +} + +export function ScriptTemplateListView({ onEdit, onCreate }: Props) { + const [templates, setTemplates] = useState([]) + const [categories, setCategories] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [searchQuery, setSearchQuery] = useState('') + const [deleteConfirm, setDeleteConfirm] = useState(null) + + const { canManageScriptTemplate, canCreateScriptTemplate } = usePermissions() + + const loadData = async () => { + setIsLoading(true) + try { + const [tpls, cats] = await Promise.all([ + scriptsApi.getManagedTemplates(searchQuery ? { search: searchQuery } : undefined), + scriptsApi.getCategories(), + ]) + setTemplates(tpls) + setCategories(cats) + } catch { + // silently fail + } finally { + setIsLoading(false) + } + } + + useEffect(() => { + loadData() + }, []) // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + const timer = setTimeout(() => { + loadData() + }, 300) + return () => clearTimeout(timer) + }, [searchQuery]) // eslint-disable-line react-hooks/exhaustive-deps + + const handleDelete = async (id: string) => { + try { + await scriptsApi.deleteTemplate(id) + setTemplates(prev => prev.filter(t => t.id !== id)) + setDeleteConfirm(null) + } catch { + // silently fail + } + } + + const getCategoryName = (categoryId: string) => + categories.find(c => c.id === categoryId)?.name ?? 'Unknown' + + return ( +
+ {/* Header row */} +
+
+

Manage Templates

+

+ Create and edit PowerShell script templates. +

+
+ {canCreateScriptTemplate && ( + + )} +
+ + {/* Search */} +
+ + setSearchQuery(e.target.value)} + placeholder="Search templates…" + className="w-full pl-8 pr-3 py-1.5 text-sm rounded-md border border-border bg-card text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-[rgba(6,182,212,0.3)] focus:ring-1 focus:ring-[rgba(6,182,212,0.2)]" + /> +
+ + {/* Template list */} + {isLoading ? ( +
+ +
+ ) : templates.length === 0 ? ( +
+ +

+ {searchQuery ? 'No templates match your search' : 'No templates yet. Create your first one!'} +

+
+ ) : ( +
+ + + + + + + + + + + + + {templates.map(t => ( + + + + + + + + + ))} + +
NameCategoryComplexityScopeUsesActions
+ {t.name} + {t.description && ( +

{t.description}

+ )} +
{getCategoryName(t.category_id)} + + {t.complexity} + + + + {t.team_id ? <> Team : <> Personal} + + {t.usage_count} +
+ {canManageScriptTemplate(t) && ( + <> + + {deleteConfirm === t.id ? ( +
+ + +
+ ) : ( + + )} + + )} +
+
+
+ )} +
+ ) +}