Move completed plan docs to docs/plans/archive/. Add survey migration 046 and reference HTML/plan files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
24 KiB
Step Library Page Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Wire up the existing Step Library components into a fully functional standalone page — replacing the "Coming Soon" stub — with create, edit, delete, preview, and save-to-library actions.
Architecture: StepLibraryPage owns all modal state and orchestrates four components: StepLibraryBrowser (list + filters), StepFormModal (new wrapper for create/edit), StepDetailModal (already exists), and a delete confirmation dialog. StepCard and StepLibraryBrowser get new optional props for library-page-specific actions. No new API calls beyond what already exists in stepsApi.
Tech Stack: React 19, TypeScript, Tailwind CSS, Zustand (useAuthStore for current user ID), stepsApi + stepCategoriesApi (all endpoints already wired to backend).
Key Files Reference
frontend/src/pages/StepLibraryPage.tsx— currently a "Coming Soon" stub; will be rewrittenfrontend/src/components/step-library/StepLibraryBrowser.tsx— list + filters componentfrontend/src/components/step-library/StepCard.tsx— individual step cardfrontend/src/components/step-library/StepDetailModal.tsx— preview modal (already complete)frontend/src/components/step-library/StepForm.tsx— create/edit form (already complete)frontend/src/api/steps.ts—stepsApi.create,.get,.update,.deletealready implementedfrontend/src/types/step.ts—Step,StepListItem,StepCreate,StepUpdatetypesfrontend/src/store/authStore.ts— useuseAuthStore((s) => s.user)to get current userfrontend/src/hooks/usePermissions.ts—canCreateStepsalready defined
How to Get Current User ID
import { useAuthStore } from '@/store/authStore'
const user = useAuthStore((s) => s.user)
// user.id is the current user's UUID string
Task 1: Extend StepCard with library-page actions
Files:
- Modify:
frontend/src/components/step-library/StepCard.tsx
This task adds onEdit, onDelete, onSave, and currentUserId props. When on the library page (these props are present), the action buttons change based on ownership.
Step 1: Read the current file
Already read above. Current interface:
interface StepCardProps {
step: StepListItem
onPreview: (step: StepListItem) => void
onInsert: (step: StepListItem) => void
}
Step 2: Update the interface and button logic
Replace the StepCardProps interface and the Actions section at the bottom of StepCard.tsx.
New interface (all new props are optional so existing CustomStepModal usage is unchanged):
interface StepCardProps {
step: StepListItem
onPreview: (step: StepListItem) => void
onInsert?: (step: StepListItem) => void // session context (existing)
onEdit?: (step: StepListItem) => void // library page
onDelete?: (stepId: string) => void // library page
onSave?: (step: StepListItem) => void // library page (save copy)
currentUserId?: string // to determine ownership
}
Replace the Actions section (the <div className="flex gap-2"> at the bottom) with:
{/* Actions */}
<div className="flex gap-2">
{/* Library page context */}
{(onEdit || onDelete || onSave) ? (
isOwn ? (
// Own step: Preview + Edit + Delete icon
<>
<button
onClick={() => onPreview(step)}
className="flex flex-1 items-center justify-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
>
<Eye className="h-4 w-4" />
Preview
</button>
<button
onClick={() => onEdit?.(step)}
className="flex flex-1 items-center justify-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
>
<Pencil className="h-4 w-4" />
Edit
</button>
<button
onClick={() => onDelete?.(step.id)}
className="flex items-center justify-center rounded-md border border-border px-3 py-2 text-muted-foreground hover:bg-red-400/10 hover:text-red-400 hover:border-red-400/30 transition-colors"
aria-label="Delete step"
>
<Trash2 className="h-4 w-4" />
</button>
</>
) : (
// Others' step: Preview + Save
<>
<button
onClick={() => onPreview(step)}
className="flex flex-1 items-center justify-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
>
<Eye className="h-4 w-4" />
Preview
</button>
<button
onClick={() => onSave?.(step)}
className="flex flex-1 items-center justify-center gap-2 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-3 py-2 text-sm font-medium hover:opacity-90 transition-colors"
>
<Bookmark className="h-4 w-4" />
Save
</button>
</>
)
) : (
// Session context (original): Preview + Insert
<>
<button
onClick={() => onPreview(step)}
className="flex flex-1 items-center justify-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
>
<Eye className="h-4 w-4" />
Preview
</button>
<button
onClick={() => onInsert?.(step)}
className="flex flex-1 items-center justify-center gap-2 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-3 py-2 text-sm font-medium hover:opacity-90 transition-colors"
>
<Plus className="h-4 w-4" />
Insert
</button>
</>
)}
</div>
Add isOwn derived value near top of the component function (before the return):
const isOwn = currentUserId ? step.created_by === currentUserId : false
Add new imports at top of file:
import { Star, User, Calendar, TrendingUp, Eye, Plus, HelpCircle, Zap, CheckCircle, Pencil, Trash2, Bookmark } from 'lucide-react'
Step 3: Verify TypeScript compiles
cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -30
Expected: no errors related to StepCard.
Step 4: Commit
cd /home/michaelchihlas/dev/patherly
git add frontend/src/components/step-library/StepCard.tsx
git commit -m "feat: add library-page action props to StepCard (edit/delete/save)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
Task 2: Extend StepLibraryBrowser with library-page props
Files:
- Modify:
frontend/src/components/step-library/StepLibraryBrowser.tsx
Pass the new onEdit, onDelete, onSave, currentUserId props through from browser to each StepCard. Also expose a refreshKey prop so the page can trigger a reload after create/edit/delete/save.
Step 1: Update the interface
Current interface:
interface StepLibraryBrowserProps {
onInsert: (step: Step) => void
onCreateNew?: () => void
showCreateButton?: boolean
}
New interface:
interface StepLibraryBrowserProps {
onInsert?: (step: Step) => void // now optional (not needed on library page)
onCreateNew?: () => void
showCreateButton?: boolean
onEdit?: (step: StepListItem) => void
onDelete?: (stepId: string) => void
onSave?: (step: StepListItem) => void
currentUserId?: string
refreshKey?: number // increment to trigger reload
}
Step 2: Wire refreshKey into the steps useEffect
In the existing useEffect that calls loadSteps, add refreshKey to the dependency array:
useEffect(() => {
const loadSteps = async () => { ... }
loadSteps()
}, [searchQuery, selectedCategoryId, selectedStepType, minRating, sortBy, selectedTag, refreshKey])
Step 3: Pass new props to StepCard
In all three groupedSteps.private/team/public map blocks, update StepCard usage:
<StepCard
key={step.id}
step={step}
onPreview={handlePreview}
onInsert={onInsert ? handleInsertFromCard : undefined}
onEdit={onEdit}
onDelete={onDelete}
onSave={onSave}
currentUserId={currentUserId}
/>
Step 4: Verify TypeScript compiles
cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -30
Step 5: Commit
cd /home/michaelchihlas/dev/patherly
git add frontend/src/components/step-library/StepLibraryBrowser.tsx
git commit -m "feat: pass library-page action props through StepLibraryBrowser + refreshKey
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
Task 3: Create StepFormModal
Files:
- Create:
frontend/src/components/step-library/StepFormModal.tsx
A thin modal wrapper around the existing StepForm. Handles both create and edit modes.
Step 1: Create the file
import { useState } from 'react'
import { X } from 'lucide-react'
import { stepsApi } from '@/api/steps'
import { StepForm } from './StepForm'
import type { Step, StepCreate, StepListItem } from '@/types/step'
interface StepFormModalProps {
isOpen: boolean
onClose: () => void
onSuccess: (step: Step) => void
editingStep?: StepListItem | null // if set, edit mode; if null/undefined, create mode
}
export function StepFormModal({ isOpen, onClose, onSuccess, editingStep }: StepFormModalProps) {
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
if (!isOpen) return null
const isEditMode = !!editingStep
const handleSubmit = async (data: StepCreate) => {
setIsSubmitting(true)
setError(null)
try {
let result: Step
if (isEditMode && editingStep) {
result = await stepsApi.update(editingStep.id, data)
} else {
result = await stepsApi.create(data)
}
onSuccess(result)
} catch (err) {
console.error('Failed to save step:', err)
setError('Failed to save step. Please try again.')
} finally {
setIsSubmitting(false)
}
}
// Build initialData from editingStep for edit mode
// StepListItem doesn't have `content`, so for edit we need to fetch full step
// This is handled by the parent (StepLibraryPage fetches full step before opening modal)
const initialData = editingStep ? {
title: editingStep.title,
step_type: editingStep.step_type as 'decision' | 'action' | 'solution',
visibility: editingStep.visibility as 'private' | 'team' | 'public',
category_id: editingStep.category_id,
tags: editingStep.tags,
} : undefined
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
<div className="relative flex h-[90vh] w-full max-w-2xl flex-col bg-card border border-border rounded-2xl shadow-lg">
{/* Header */}
<div className="flex items-center justify-between border-b border-border p-6 pb-4">
<h2 className="text-lg font-semibold text-foreground">
{isEditMode ? 'Edit Step' : 'Create Step'}
</h2>
<button
onClick={onClose}
disabled={isSubmitting}
className="rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50"
aria-label="Close"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Error */}
{error && (
<div className="mx-6 mt-4 rounded-lg border border-red-400/20 bg-red-400/10 p-3 text-sm text-red-400">
{error}
</div>
)}
{/* Body */}
<div className="flex-1 overflow-y-auto p-6">
<StepForm
onSubmit={handleSubmit}
onCancel={onClose}
initialData={initialData}
submitLabel={isEditMode ? 'Save Changes' : 'Create Step'}
isSubmitting={isSubmitting}
/>
</div>
</div>
</div>
)
}
Step 2: Update StepForm to accept submitLabel and isSubmitting props
StepForm currently has a hardcoded submit button label ("Insert Step") and no loading state. Add these two optional props:
interface StepFormProps {
onSubmit: (data: StepCreate) => void
onCancel: () => void
initialData?: Partial<StepCreate>
submitLabel?: string // default: 'Insert Step'
isSubmitting?: boolean // default: false
}
In StepForm, use them on the submit button:
<button
type="submit"
disabled={isSubmitting}
className="flex-1 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50"
>
{isSubmitting ? 'Saving...' : (submitLabel ?? 'Insert Step')}
</button>
Step 3: Verify TypeScript compiles
cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -30
Step 4: Commit
cd /home/michaelchihlas/dev/patherly
git add frontend/src/components/step-library/StepFormModal.tsx \
frontend/src/components/step-library/StepForm.tsx
git commit -m "feat: StepFormModal wrapper + submitLabel/isSubmitting props on StepForm
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
Task 4: Rewrite StepLibraryPage
Files:
- Modify:
frontend/src/pages/StepLibraryPage.tsx
This is the main wiring task. Replace the stub with the full page.
Step 1: Write the new page
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()
// Modal state
const [createOpen, setCreateOpen] = useState(false)
const [editingStep, setEditingStep] = useState<StepListItem | null>(null)
const [deletingStep, setDeletingStep] = useState<StepListItem | null>(null)
const [isDeleting, setIsDeleting] = useState(false)
const [deleteError, setDeleteError] = useState<string | null>(null)
const [saveToast, setSaveToast] = useState<string | null>(null)
// Increment to trigger StepLibraryBrowser reload
const [refreshKey, setRefreshKey] = useState(0)
const refresh = () => setRefreshKey(k => k + 1)
const handleEdit = (step: StepListItem) => {
setEditingStep(step)
}
const handleDeleteRequest = (stepId: string) => {
// Find the step in order to show its title in the confirmation
// We store the StepListItem via the browser's onDelete callback
// The step object is passed from StepCard which has the full StepListItem
setDeletingStep({ id: stepId } as StepListItem)
}
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 {
// Fetch full step to get content fields
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()
}
return (
<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-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>
{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>
{/* Browser fills remaining height */}
<div className="flex-1 overflow-hidden">
<StepLibraryBrowser
onEdit={handleEdit}
onDelete={(stepId) => {
// We need the full StepListItem for the confirmation title.
// Pass a minimal object; the title will show as "this step" if not available.
setDeletingStep({ id: stepId, title: '' } as StepListItem)
}}
onSave={handleSave}
currentUserId={user?.id}
refreshKey={refreshKey}
showCreateButton={false}
/>
</div>
{/* Create / Edit Modal */}
<StepFormModal
isOpen={createOpen || !!editingStep}
onClose={() => { setCreateOpen(false); setEditingStep(null) }}
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">
{deletingStep.title
? <>Are you sure you want to delete <span className="font-medium text-foreground">"{deletingStep.title}"</span>?</>
: 'Are you sure you want to delete this step?'
}
</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>
)
}
NOTE on delete title: The onDelete callback from StepCard only passes stepId: string, not the full StepListItem. To show the step title in the confirmation dialog, change the StepLibraryBrowser's onDelete prop type to pass the full StepListItem instead:
In StepLibraryBrowser.tsx, change:
onDelete?: (stepId: string) => void
to:
onDelete?: (step: StepListItem) => void
And update where it calls onDelete from cards — pass the full step object. Update StepCard similarly: change onDelete?: (stepId: string) => void to onDelete?: (step: StepListItem) => void and call onDelete?.(step) instead of onDelete?.(step.id).
Then in StepLibraryPage, use handleDeleteRequest(step: StepListItem) and set setDeletingStep(step) directly — no need to pass a minimal object.
Step 2: Run TypeScript check
cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -40
Fix any type errors before proceeding.
Step 3: Run build
cd /home/michaelchihlas/dev/patherly/frontend && npm run build 2>&1 | tail -20
Expected: build succeeds with no errors.
Step 4: Commit
cd /home/michaelchihlas/dev/patherly
git add frontend/src/pages/StepLibraryPage.tsx \
frontend/src/components/step-library/StepLibraryBrowser.tsx \
frontend/src/components/step-library/StepCard.tsx
git commit -m "feat: Step Library page — create, edit, delete, save-to-library
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
Task 5: Manual verification checklist
Start the dev server and verify these flows work end-to-end:
docker start patherly_postgres
cd /home/michaelchihlas/dev/patherly/backend && source venv/bin/activate && uvicorn app.main:app --reload &
cd /home/michaelchihlas/dev/patherly/frontend && npm run dev
Navigate to http://localhost:5173/step-library and verify:
- Page loads without errors (not "Coming Soon")
- "+ Create Step" button appears (login as engineer or admin)
- Creating a step via the modal saves it and it appears under "My Steps" on reload
- "Edit" button appears on your own step cards
- Editing a step opens the form pre-filled (note:
contentfields won't pre-fill sinceStepListItemdoesn't have content — this is acceptable for now; see note below) - "Delete" button appears on your own step cards
- Delete confirmation shows step title; confirming removes it from the list
- "Save" button appears on team/community step cards
- Saving a step copies it to "My Steps" and shows toast
- "Preview" opens
StepDetailModalcorrectly on all card types - Filters (category, type, rating, sort) work
- Popular tags clickable and filter results
Note on edit pre-fill: StepListItem does not include content. The StepFormModal passes initialData from editingStep, but content will be missing. For a full pre-fill, StepLibraryPage.handleEdit should fetch the full step via stepsApi.get(step.id) before opening the modal, and store the result as a Step (not StepListItem) in editingStep state. Update editingStep state type to Step | null and fetch in handleEdit:
const [editingStep, setEditingStep] = useState<Step | null>(null)
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)
}
}
Update StepFormModal's editingStep prop type to accept Step | null and build initialData from the full Step including content:
editingStep?: Step | null
const initialData = editingStep ? {
title: editingStep.title,
step_type: editingStep.step_type,
content: editingStep.content,
visibility: editingStep.visibility,
category_id: editingStep.category_id,
tags: editingStep.tags,
} : undefined
This should be done as part of Task 4 before verifying.
Task 6: Final build validation and commit
cd /home/michaelchihlas/dev/patherly/frontend && npm run build 2>&1 | tail -20
Expected: clean build, no TypeScript errors, no warnings about missing exports.
If clean:
cd /home/michaelchihlas/dev/patherly
git add -A
git status # confirm only expected files changed
git commit -m "chore: step library page final build validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"