706 lines
24 KiB
Markdown
706 lines
24 KiB
Markdown
# 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>"
|
|
```
|