Add step library foundation and user preferences (Issues #3, #5, #6, #7)

Issue #3: User Preferences - export format default
- Add userPreferencesStore with localStorage persistence
- Create Settings page with export format dropdown and theme toggle
- SessionDetailPage now uses default export format from preferences

Issue #5: Step Categories - database table and seed data
- Migration 007: step_categories table with team scoping
- Seed 10 default global categories (Citrix/VDI, AD, M365, etc.)
- Full CRUD API at /api/v1/step-categories

Issue #6: Step Library - database schema
- Migration 008: step_library, step_ratings, step_usage_log tables
- Support for decision/action/solution step types
- Visibility levels: private, team, public
- Rating aggregates and usage tracking

Issue #7: Step Library - CRUD API endpoints
- Full CRUD at /api/v1/steps with visibility filtering
- Full-text search endpoint
- Popular tags endpoint
- Rating/review system with verified use tracking

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-03 01:25:31 -05:00
parent 1e4eec00e2
commit c782d41eff
17 changed files with 1672 additions and 2 deletions

View File

@@ -16,6 +16,7 @@ export function AppLayout() {
const navItems = [
{ path: '/trees', label: 'Trees' },
{ path: '/sessions', label: 'Sessions' },
{ path: '/settings', label: 'Settings' },
]
return (

View File

@@ -3,17 +3,19 @@ import { useParams, useNavigate } from 'react-router-dom'
import { Copy, Check, Eye } from 'lucide-react'
import { sessionsApi } from '@/api'
import { ExportPreviewModal } from '@/components/session/ExportPreviewModal'
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
import type { Session, SessionExport } from '@/types'
import { cn } from '@/lib/utils'
export function SessionDetailPage() {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
const { defaultExportFormat } = useUserPreferencesStore()
const [session, setSession] = useState<Session | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isExporting, setIsExporting] = useState(false)
const [exportFormat, setExportFormat] = useState<'markdown' | 'text' | 'html'>('markdown')
const [exportFormat, setExportFormat] = useState<'markdown' | 'text' | 'html'>(defaultExportFormat)
const [exportContent, setExportContent] = useState<string | null>(null)
const [showPreview, setShowPreview] = useState(false)
const [copied, setCopied] = useState(false)

View File

@@ -0,0 +1,93 @@
import { Settings } from 'lucide-react'
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
import { useThemeStore } from '@/store/themeStore'
import { cn } from '@/lib/utils'
import { ThemeToggle } from '@/components/common/ThemeToggle'
export function SettingsPage() {
const { defaultExportFormat, setDefaultExportFormat } = useUserPreferencesStore()
const { theme } = useThemeStore()
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<div className="flex items-center gap-3">
<Settings className="h-8 w-8 text-primary" />
<h1 className="text-3xl font-bold text-foreground">Settings</h1>
</div>
<p className="mt-2 text-muted-foreground">
Manage your application preferences
</p>
</div>
<div className="max-w-2xl space-y-6">
{/* Appearance Section */}
<div className="rounded-lg border border-border bg-card p-6 shadow-sm">
<h2 className="text-lg font-semibold text-card-foreground">Appearance</h2>
<p className="mt-1 text-sm text-muted-foreground">
Customize how Patherly looks on your device
</p>
<div className="mt-4">
<label className="block text-sm font-medium text-card-foreground">
Theme
</label>
<p className="text-sm text-muted-foreground">
Current: {theme.charAt(0).toUpperCase() + theme.slice(1)}
</p>
<div className="mt-2">
<ThemeToggle />
</div>
</div>
</div>
{/* Export Preferences Section */}
<div className="rounded-lg border border-border bg-card p-6 shadow-sm">
<h2 className="text-lg font-semibold text-card-foreground">Export Preferences</h2>
<p className="mt-1 text-sm text-muted-foreground">
Configure default settings for session exports
</p>
<div className="mt-4">
<label
htmlFor="export-format"
className="block text-sm font-medium text-card-foreground"
>
Default Export Format
</label>
<p className="text-sm text-muted-foreground">
This format will be pre-selected when exporting sessions
</p>
<select
id="export-format"
value={defaultExportFormat}
onChange={(e) => setDefaultExportFormat(e.target.value as 'markdown' | 'text' | 'html')}
className={cn(
'mt-2 block w-full rounded-md border border-input bg-background px-3 py-2',
'text-sm text-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
)}
>
<option value="markdown">Markdown (.md)</option>
<option value="text">Plain Text (.txt)</option>
<option value="html">HTML (.html)</option>
</select>
</div>
</div>
{/* About Section */}
<div className="rounded-lg border border-border bg-card p-6 shadow-sm">
<h2 className="text-lg font-semibold text-card-foreground">About</h2>
<p className="mt-1 text-sm text-muted-foreground">
Patherly - Troubleshooting Decision Trees
</p>
<p className="mt-2 text-sm text-muted-foreground">
"Take the path MOST traveled."
</p>
</div>
</div>
</div>
)
}
export default SettingsPage

View File

@@ -5,3 +5,4 @@ export { default as TreeNavigationPage } from './TreeNavigationPage'
export { default as TreeEditorPage } from './TreeEditorPage'
export { default as SessionHistoryPage } from './SessionHistoryPage'
export { default as SessionDetailPage } from './SessionDetailPage'
export { default as SettingsPage } from './SettingsPage'

View File

@@ -9,6 +9,7 @@ import {
TreeEditorPage,
SessionHistoryPage,
SessionDetailPage,
SettingsPage,
} from '@/pages'
export const router = createBrowserRouter([
@@ -59,6 +60,10 @@ export const router = createBrowserRouter([
path: 'sessions/:id',
element: <SessionDetailPage />,
},
{
path: 'settings',
element: <SettingsPage />,
},
],
},
])

View File

@@ -0,0 +1,23 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
type ExportFormat = 'markdown' | 'text' | 'html'
interface UserPreferencesState {
defaultExportFormat: ExportFormat
setDefaultExportFormat: (format: ExportFormat) => void
}
export const useUserPreferencesStore = create<UserPreferencesState>()(
persist(
(set) => ({
defaultExportFormat: 'markdown',
setDefaultExportFormat: (format) => set({ defaultExportFormat: format }),
}),
{
name: 'user-preferences-storage',
}
)
)
export default useUserPreferencesStore