Files
resolutionflow/docs/plans/archive/2026-02-24-step-library-page-plan.md
chihlasm 932927b9df chore: archive old plan docs + add survey foundation files
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>
2026-03-05 02:03:38 -05:00

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 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.tsstepsApi.create, .get, .update, .delete already implemented
  • frontend/src/types/step.tsStep, StepListItem, StepCreate, StepUpdate types
  • frontend/src/store/authStore.ts — use useAuthStore((s) => s.user) to get current user
  • frontend/src/hooks/usePermissions.tscanCreateSteps already 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: 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:

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>"