docs: add Step Library and Procedural Custom Steps design/plan docs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
705
docs/plans/2026-02-24-step-library-page-plan.md
Normal file
705
docs/plans/2026-02-24-step-library-page-plan.md
Normal file
@@ -0,0 +1,705 @@
|
||||
# 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 rewritten
|
||||
- `frontend/src/components/step-library/StepLibraryBrowser.tsx` — list + filters component
|
||||
- `frontend/src/components/step-library/StepCard.tsx` — individual step card
|
||||
- `frontend/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`, `.delete` already implemented
|
||||
- `frontend/src/types/step.ts` — `Step`, `StepListItem`, `StepCreate`, `StepUpdate` types
|
||||
- `frontend/src/store/authStore.ts` — use `useAuthStore((s) => s.user)` to get current user
|
||||
- `frontend/src/hooks/usePermissions.ts` — `canCreateSteps` already defined
|
||||
|
||||
---
|
||||
|
||||
## How to Get Current User ID
|
||||
|
||||
```tsx
|
||||
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:
|
||||
```ts
|
||||
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):
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
{/* 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):
|
||||
```tsx
|
||||
const isOwn = currentUserId ? step.created_by === currentUserId : false
|
||||
```
|
||||
|
||||
Add new imports at top of file:
|
||||
```tsx
|
||||
import { Star, User, Calendar, TrendingUp, Eye, Plus, HelpCircle, Zap, CheckCircle, Pencil, Trash2, Bookmark } from 'lucide-react'
|
||||
```
|
||||
|
||||
**Step 3: Verify TypeScript compiles**
|
||||
|
||||
```bash
|
||||
cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -30
|
||||
```
|
||||
|
||||
Expected: no errors related to StepCard.
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
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:
|
||||
```ts
|
||||
interface StepLibraryBrowserProps {
|
||||
onInsert: (step: Step) => void
|
||||
onCreateNew?: () => void
|
||||
showCreateButton?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
New interface:
|
||||
```ts
|
||||
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:
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
<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**
|
||||
|
||||
```bash
|
||||
cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -30
|
||||
```
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```ts
|
||||
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:
|
||||
```tsx
|
||||
<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**
|
||||
|
||||
```bash
|
||||
cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -30
|
||||
```
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```tsx
|
||||
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:
|
||||
```ts
|
||||
onDelete?: (stepId: string) => void
|
||||
```
|
||||
to:
|
||||
```ts
|
||||
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**
|
||||
|
||||
```bash
|
||||
cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -40
|
||||
```
|
||||
|
||||
Fix any type errors before proceeding.
|
||||
|
||||
**Step 3: Run build**
|
||||
|
||||
```bash
|
||||
cd /home/michaelchihlas/dev/patherly/frontend && npm run build 2>&1 | tail -20
|
||||
```
|
||||
|
||||
Expected: build succeeds with no errors.
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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: `content` fields won't pre-fill since `StepListItem` doesn'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 `StepDetailModal` correctly 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`:
|
||||
|
||||
```tsx
|
||||
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`:
|
||||
|
||||
```tsx
|
||||
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
|
||||
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
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>"
|
||||
```
|
||||
Reference in New Issue
Block a user