feat: Step Library page — create, edit, delete, save-to-library

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-24 20:46:17 -05:00
parent 8f46e0e227
commit 3f6e126f97

View File

@@ -1,25 +1,179 @@
import { Bookmark } from 'lucide-react'
import { useState } from 'react'
import { Bookmark, Trash2 } from 'lucide-react'
import { useAuthStore } from '@/store/authStore'
import { usePermissions } from '@/hooks/usePermissions'
import { stepsApi } from '@/api/steps'
import { StepLibraryBrowser } from '@/components/step-library/StepLibraryBrowser'
import { StepFormModal } from '@/components/step-library/StepFormModal'
import type { Step, StepListItem } from '@/types/step'
export default function StepLibraryPage() {
const user = useAuthStore((s) => s.user)
const { canCreateSteps } = usePermissions()
// Create/edit modal state
const [createOpen, setCreateOpen] = useState(false)
const [editingStep, setEditingStep] = useState<Step | null>(null)
// Delete confirmation state
const [deletingStep, setDeletingStep] = useState<StepListItem | null>(null)
const [isDeleting, setIsDeleting] = useState(false)
const [deleteError, setDeleteError] = useState<string | null>(null)
// Toast for "Save to My Library"
const [saveToast, setSaveToast] = useState<string | null>(null)
// Increment to trigger StepLibraryBrowser reload
const [refreshKey, setRefreshKey] = useState(0)
const refresh = () => setRefreshKey(k => k + 1)
// Fetch full step before opening edit modal (StepListItem lacks content)
const handleEdit = async (step: StepListItem) => {
try {
const full = await stepsApi.get(step.id)
setEditingStep(full)
} catch (err) {
console.error('Failed to load step for edit:', err)
}
}
const handleDeleteRequest = (step: StepListItem) => {
setDeletingStep(step)
}
const handleDeleteConfirm = async () => {
if (!deletingStep) return
setIsDeleting(true)
setDeleteError(null)
try {
await stepsApi.delete(deletingStep.id)
setDeletingStep(null)
refresh()
} catch (err) {
console.error('Failed to delete step:', err)
setDeleteError('Failed to delete step. Please try again.')
} finally {
setIsDeleting(false)
}
}
const handleSave = async (step: StepListItem) => {
try {
const full = await stepsApi.get(step.id)
await stepsApi.create({
title: full.title,
step_type: full.step_type,
content: full.content,
visibility: 'private',
category_id: full.category_id,
tags: full.tags,
})
setSaveToast(`"${full.title}" saved to My Steps`)
setTimeout(() => setSaveToast(null), 3000)
refresh()
} catch (err) {
console.error('Failed to save step:', err)
}
}
const handleFormSuccess = (_step: Step) => {
setCreateOpen(false)
setEditingStep(null)
refresh()
}
const handleCloseModal = () => {
setCreateOpen(false)
setEditingStep(null)
}
return (
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<div className="mb-8">
<div className="flex h-full flex-col">
{/* Page Header */}
<div className="flex items-center justify-between border-b border-border px-6 py-4">
<div className="flex items-center gap-3">
<span title="Step Library"><Bookmark className="h-8 w-8 text-muted-foreground" /></span>
<h1 className="text-2xl font-bold font-heading text-foreground sm:text-3xl">Step Library</h1>
<span title="Step Library">
<Bookmark className="h-6 w-6 text-muted-foreground" />
</span>
<div>
<h1 className="text-xl font-bold font-heading text-foreground">Step Library</h1>
<p className="text-sm text-muted-foreground">Reusable steps you can insert into any flow</p>
</div>
</div>
<p className="mt-2 text-muted-foreground">Reusable steps for your flows coming soon.</p>
{canCreateSteps && (
<button
onClick={() => setCreateOpen(true)}
className="rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20 hover:opacity-90"
>
+ Create Step
</button>
)}
</div>
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="rounded-full bg-primary/10 p-4 mb-4">
<Bookmark className="h-8 w-8 text-primary" />
</div>
<h2 className="text-lg font-semibold text-foreground mb-2">Coming Soon</h2>
<p className="max-w-md text-sm text-muted-foreground">
The Step Library will let you create, share, and reuse common troubleshooting steps across all your flows.
</p>
{/* Browser fills remaining height */}
<div className="flex-1 overflow-hidden">
<StepLibraryBrowser
onEdit={(step) => { handleEdit(step) }}
onDelete={handleDeleteRequest}
onSave={handleSave}
currentUserId={user?.id}
refreshKey={refreshKey}
showCreateButton={false}
/>
</div>
{/* Create / Edit Modal */}
<StepFormModal
isOpen={createOpen || !!editingStep}
onClose={handleCloseModal}
onSuccess={handleFormSuccess}
editingStep={editingStep}
/>
{/* Delete Confirmation Dialog */}
{deletingStep && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
<div className="w-full max-w-sm rounded-xl bg-card border border-border p-6 shadow-lg">
<div className="mb-4 flex items-center gap-3">
<div className="rounded-full bg-red-400/10 p-2">
<Trash2 className="h-5 w-5 text-red-400" />
</div>
<h2 className="text-base font-semibold text-foreground">Delete Step</h2>
</div>
<p className="mb-2 text-sm text-muted-foreground">
Are you sure you want to delete{' '}
<span className="font-medium text-foreground">"{deletingStep.title}"</span>?
</p>
<p className="mb-6 text-xs text-muted-foreground">This cannot be undone.</p>
{deleteError && (
<p className="mb-4 text-sm text-red-400">{deleteError}</p>
)}
<div className="flex gap-2">
<button
onClick={() => { setDeletingStep(null); setDeleteError(null) }}
disabled={isDeleting}
className="flex-1 rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50"
>
Cancel
</button>
<button
onClick={handleDeleteConfirm}
disabled={isDeleting}
className="flex-1 rounded-md bg-red-500 px-4 py-2 text-sm font-medium text-white hover:bg-red-600 disabled:opacity-50"
>
{isDeleting ? 'Deleting...' : 'Delete'}
</button>
</div>
</div>
</div>
)}
{/* Save Toast */}
{saveToast && (
<div className="fixed bottom-6 left-1/2 z-50 -translate-x-1/2 rounded-lg border border-border bg-card px-4 py-2 text-sm text-foreground shadow-lg">
{saveToast}
</div>
)}
</div>
)
}