Added 'All Scripts' as the first/default tab (no mine/shared filter) so the page opens with every script the user can access. My Scripts and Team Scripts tabs remain for filtered views. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
207 lines
7.2 KiB
TypeScript
207 lines
7.2 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import { Terminal, Settings, Wand2, FileUp } from 'lucide-react'
|
|
import { useScriptGeneratorStore } from '@/store/scriptGeneratorStore'
|
|
import { usePermissions } from '@/hooks/usePermissions'
|
|
import { ScriptFilterBar } from '@/components/scripts/ScriptFilterBar'
|
|
import { ScriptTemplateList } from '@/components/scripts/ScriptTemplateList'
|
|
import { ScriptConfigurePane } from '@/components/scripts/ScriptConfigurePane'
|
|
import { ScriptPreview } from '@/components/scripts/ScriptPreview'
|
|
import { ParameterizeAndSavePanel } from '@/components/scripts/ParameterizeAndSavePanel'
|
|
import { scriptsApi } from '@/api'
|
|
import type { ScriptParameter } from '@/types'
|
|
|
|
type LibraryTab = 'all' | 'mine' | 'team'
|
|
|
|
export default function ScriptLibraryPage() {
|
|
const [paneMode, setPaneMode] = useState<'browse' | 'configure'>('browse')
|
|
// inputValue owned here so it survives Configure ↔ Browse transitions
|
|
const [inputValue, setInputValue] = useState('')
|
|
const [activeTab, setActiveTab] = useState<LibraryTab>('all')
|
|
|
|
const loadCategories = useScriptGeneratorStore(s => s.loadCategories)
|
|
const loadTemplates = useScriptGeneratorStore(s => s.loadTemplates)
|
|
const setSearch = useScriptGeneratorStore(s => s.setSearch)
|
|
const selectTemplate = useScriptGeneratorStore(s => s.selectTemplate)
|
|
const clearOutput = useScriptGeneratorStore(s => s.clearOutput)
|
|
const selectedTemplate = useScriptGeneratorStore(s => s.selectedTemplate)
|
|
|
|
const categories = useScriptGeneratorStore(s => s.categories)
|
|
|
|
const { isEngineer } = usePermissions()
|
|
const canGenerate = isEngineer
|
|
const [showImportPanel, setShowImportPanel] = useState(false)
|
|
|
|
const handleImportSave = async (payload: {
|
|
name: string
|
|
description: string | undefined
|
|
category_id: string | undefined
|
|
share_with_team: boolean
|
|
script_body: string
|
|
parameters_schema: { parameters: ScriptParameter[] }
|
|
}) => {
|
|
const categoryId = payload.category_id || categories[0]?.id
|
|
if (!categoryId) {
|
|
throw new Error('No categories available. Please create a category first.')
|
|
}
|
|
await scriptsApi.createTemplate({
|
|
category_id: categoryId,
|
|
name: payload.name,
|
|
description: payload.description,
|
|
script_body: payload.script_body,
|
|
parameters_schema: payload.parameters_schema,
|
|
})
|
|
setShowImportPanel(false)
|
|
const filters =
|
|
activeTab === 'mine' ? { mine: true } :
|
|
activeTab === 'team' ? { shared: true } :
|
|
{}
|
|
loadTemplates(filters)
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadCategories().then(() => {
|
|
const filters =
|
|
activeTab === 'mine' ? { mine: true } :
|
|
activeTab === 'team' ? { shared: true } :
|
|
{}
|
|
loadTemplates(filters)
|
|
})
|
|
}, [loadCategories, loadTemplates, activeTab])
|
|
|
|
const onTabChange = (tab: LibraryTab) => {
|
|
setActiveTab(tab)
|
|
setInputValue('')
|
|
setSearch('')
|
|
setPaneMode('browse')
|
|
}
|
|
|
|
const onClearSearch = () => {
|
|
setInputValue('')
|
|
setSearch('')
|
|
}
|
|
|
|
const onConfigure = (id: string) => {
|
|
selectTemplate(id)
|
|
setPaneMode('configure')
|
|
}
|
|
|
|
const onBack = () => {
|
|
clearOutput()
|
|
setPaneMode('browse')
|
|
}
|
|
|
|
const tabClass = (tab: LibraryTab) =>
|
|
tab === activeTab
|
|
? 'border-b-2 border-primary text-foreground'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4 p-6 h-full">
|
|
{/* Page header */}
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-heading font-bold text-foreground">Script Library</h1>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
Browse templates, fill in parameters, and generate ready-to-run scripts.
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{isEngineer && (
|
|
<>
|
|
<Link
|
|
to="/scripts/manage"
|
|
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground px-2.5 py-1.5 rounded-lg transition-colors"
|
|
>
|
|
<Settings size={12} />
|
|
Manage
|
|
</Link>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowImportPanel(true)}
|
|
className="inline-flex items-center gap-1.5 rounded-lg border border-border px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:border-[var(--color-border-hover)] transition-colors"
|
|
>
|
|
<FileUp size={14} />
|
|
Import Script
|
|
</button>
|
|
</>
|
|
)}
|
|
<Link
|
|
to="/script-builder"
|
|
className="inline-flex items-center gap-2 bg-primary text-white font-semibold rounded-lg px-4 py-2 text-sm hover:brightness-110 active:scale-[0.98] transition-all"
|
|
>
|
|
<Wand2 size={14} />
|
|
Build New Script
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tab bar */}
|
|
<div className="flex gap-6 border-b border-border">
|
|
<button
|
|
type="button"
|
|
onClick={() => onTabChange('all')}
|
|
className={`pb-2 text-sm font-medium transition-colors ${tabClass('all')}`}
|
|
>
|
|
All Scripts
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => onTabChange('mine')}
|
|
className={`pb-2 text-sm font-medium transition-colors ${tabClass('mine')}`}
|
|
>
|
|
My Scripts
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => onTabChange('team')}
|
|
className={`pb-2 text-sm font-medium transition-colors ${tabClass('team')}`}
|
|
>
|
|
Team Scripts
|
|
</button>
|
|
</div>
|
|
|
|
{/* Two-column layout */}
|
|
<div className="grid grid-cols-[320px_1fr] gap-4 flex-1 min-h-0">
|
|
{/* Left pane — Browse or Configure mode */}
|
|
{paneMode === 'browse' ? (
|
|
<div className="card-flat flex flex-col overflow-hidden">
|
|
<div className="p-2 pb-0">
|
|
<ScriptFilterBar inputValue={inputValue} setInputValue={setInputValue} />
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto">
|
|
<ScriptTemplateList
|
|
inputValue={inputValue}
|
|
onClearSearch={onClearSearch}
|
|
onConfigure={onConfigure}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<ScriptConfigurePane canGenerate={canGenerate} onBack={onBack} />
|
|
)}
|
|
|
|
{/* Right pane — read-only ScriptPreview */}
|
|
{selectedTemplate === null ? (
|
|
<div className="card-flat h-full flex flex-col items-center justify-center gap-3 text-center p-8">
|
|
<Terminal size={40} className="text-muted-foreground/40" />
|
|
<p className="text-sm text-muted-foreground">Select a template to get started</p>
|
|
</div>
|
|
) : (
|
|
<div className="card-flat h-full overflow-y-auto p-4">
|
|
<ScriptPreview />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Import script panel */}
|
|
{showImportPanel && (
|
|
<ParameterizeAndSavePanel
|
|
onSave={handleImportSave}
|
|
onClose={() => setShowImportPanel(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|