Files
resolutionflow/frontend/src/components/script-editor/ScriptTemplateListView.tsx
Michael Chihlas 123fc50af9 fix: replace all remaining old brand tokens (text-brand-dark, border-brand-border, bg-white opacity)
30 references to removed CSS variables: border-brand-border → border-[#1e2130],
text-brand-text-muted → text-[#4f5666], text-brand-dark → text-white,
bg-white/[0.04] → bg-[#191c25], hover:border-white/[0.12] → hover:border-[#2a2f3d].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 04:27:46 -04:00

218 lines
9.2 KiB
TypeScript

import { useState, useEffect } from 'react'
import { Plus, Search, Pencil, Trash2, Users, User as UserIcon, Loader2, FileCode, ArrowLeft } from 'lucide-react'
import { Link } from 'react-router-dom'
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<ScriptTemplateListItem[]>([])
const [categories, setCategories] = useState<ScriptCategoryResponse[]>([])
const [isLoading, setIsLoading] = useState(true)
const [searchQuery, setSearchQuery] = useState('')
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(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 (
<div className="flex flex-col gap-4">
{/* Back link */}
<Link
to="/scripts"
className="flex items-center gap-1.5 text-xs text-[#848b9b] hover:text-[#e2e5eb] transition-colors w-fit"
>
<ArrowLeft size={12} />
Back to Script Library
</Link>
{/* Header row */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-heading font-bold text-[#e2e5eb]">Manage Templates</h1>
<p className="text-sm text-[#848b9b] mt-1">
Create and edit PowerShell script templates.
</p>
</div>
{canCreateScriptTemplate && (
<button
type="button"
onClick={onCreate}
className="flex items-center gap-1.5 bg-[#22d3ee] text-white font-semibold text-sm px-4 py-2 rounded-lg hover:brightness-110 active:scale-[0.98] transition-all"
>
<Plus size={16} />
New Template
</button>
)}
</div>
{/* Search */}
<div className="relative w-64">
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[#848b9b] pointer-events-none" />
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder="Search templates…"
className="w-full pl-8 pr-3 py-1.5 text-sm rounded-md border border-[#1e2130] bg-[#14161d] text-[#e2e5eb] placeholder:text-[#848b9b] focus:outline-none focus:border-[rgba(6,182,212,0.3)] focus:ring-1 focus:ring-[rgba(6,182,212,0.2)]"
/>
</div>
{/* Template list */}
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 size={28} className="text-[#22d3ee] animate-spin" />
</div>
) : templates.length === 0 ? (
<div className="card-flat flex flex-col items-center justify-center gap-3 py-12 text-center">
<FileCode size={32} className="text-[#848b9b]/40" />
<p className="text-sm text-[#848b9b]">
{searchQuery ? 'No templates match your search' : 'No templates yet. Create your first one!'}
</p>
</div>
) : (
<div className="card-flat overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1e2130]">
<th className="text-left font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b] px-4 py-3">Name</th>
<th className="text-left font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b] px-4 py-3">Category</th>
<th className="text-left font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b] px-4 py-3">Complexity</th>
<th className="text-left font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b] px-4 py-3">Scope</th>
<th className="text-right font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b] px-4 py-3">Uses</th>
<th className="text-right font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b] px-4 py-3">Actions</th>
</tr>
</thead>
<tbody>
{templates.map(t => (
<tr
key={t.id}
className="border-b border-[#1e2130] last:border-b-0 hover:bg-[#14161d] transition-colors"
>
<td className="px-4 py-3">
<span className="text-[#e2e5eb] font-medium">{t.name}</span>
{t.description && (
<p className="text-xs text-[#848b9b] line-clamp-1 mt-0.5">{t.description}</p>
)}
</td>
<td className="px-4 py-3 text-[#848b9b]">{getCategoryName(t.category_id)}</td>
<td className="px-4 py-3">
<span className={cn('font-sans text-xs text-[0.625rem] uppercase tracking-wide px-1.5 py-0.5 rounded', COMPLEXITY_CLASSES[t.complexity])}>
{t.complexity}
</span>
</td>
<td className="px-4 py-3">
<span className={cn(
'inline-flex items-center gap-1 font-sans text-xs text-[0.625rem] uppercase tracking-wide px-1.5 py-0.5 rounded border',
t.team_id
? 'text-[#22d3ee] bg-[rgba(34,211,238,0.10)] border-primary/20'
: 'text-[#848b9b] bg-white/5 border-[#1e2130]'
)}>
{t.team_id ? <><Users size={10} /> Team</> : <><UserIcon size={10} /> Personal</>}
</span>
</td>
<td className="px-4 py-3 text-right text-[#848b9b]">{t.usage_count}</td>
<td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-1">
{canManageScriptTemplate(t) && (
<>
<button
type="button"
onClick={() => onEdit(t.id)}
className="p-1.5 rounded-md text-[#848b9b] hover:text-[#e2e5eb] hover:bg-white/5 transition-colors"
title="Edit template"
>
<Pencil size={14} />
</button>
{deleteConfirm === t.id ? (
<div className="flex items-center gap-1">
<button
type="button"
onClick={() => handleDelete(t.id)}
className="text-[0.625rem] font-sans text-xs text-rose-500 hover:text-rose-400 px-1.5 py-0.5"
>
Confirm
</button>
<button
type="button"
onClick={() => setDeleteConfirm(null)}
className="text-[0.625rem] font-sans text-xs text-[#848b9b] hover:text-[#e2e5eb] px-1.5 py-0.5"
>
Cancel
</button>
</div>
) : (
<button
type="button"
onClick={() => setDeleteConfirm(t.id)}
className="p-1.5 rounded-md text-[#848b9b] hover:text-rose-500 hover:bg-white/5 transition-colors"
title="Delete template"
>
<Trash2 size={14} />
</button>
)}
</>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)
}