feat: session sharing frontend (#76)

* feat: add session sharing types, API client, and utilities

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add SessionTimeline and ActionMenu reusable components

SessionTimeline extracts timeline/checklist rendering from SessionDetailPage
into a reusable component for both authenticated and public session views.
ActionMenu provides a dropdown action menu with keyboard/click-outside dismiss.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add ShareSessionModal and integrate into SessionDetailPage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add Share Progress popover to TreeNavigationPage

Replace the single "Copy for Ticket" button with a "Share Progress"
popover that offers three actions: Copy Progress Summary (existing PSA
export flow), Copy Share Link (auto-creates account-only share if
needed), and Manage Share Links (opens ShareSessionModal).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add public SharedSessionPage with tree preview

Add the public-facing shared session page at /share/:shareToken that
renders shared sessions without authentication. Includes error handling
for 401 (redirect to login), 403 (access denied), 404 (not found),
and 410 (expired). The page features a minimal header, session metadata,
SessionTimeline component, and a new SharedSessionTreePreview component
that renders the decision tree structure with the path taken highlighted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add My Shares management page with nav link

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address code review issues in session sharing

- Add useCallback for loadShares in ShareSessionModal (React hook deps)
- Use TreeStructure type instead of Record<string, unknown> for type safety
- Fix login redirect format to match LoginPage's expected state shape

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add focused tests for session sharing utilities and API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve tree_structure type compatibility for shared session views

- Use TreeStructure & Record<string, unknown> intersection for JSONB flexibility
- Add explicit cast in SharedSessionTreePreview for recursive node rendering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add session sharing learnings to CLAUDE.md

Add gotchas #12 (TreeStructure vs Tree types) and #13 (login redirect
state format), note about npm run build strictness, and public route
pattern to Common Tasks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: procedural editor UX improvements

Add URL intake field type, fix variable name editing collapsing fields
(index-based keys/updates), auto-generate variable names by field type,
add section header as first-class step type, and simplify step editor
with "More Options" collapsible for advanced fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: allow section_header step type in validation, improve tag input

- Add 'section_header' to VALID_STEP_TYPES in backend validation so
  procedural flows with section headers can be published
- Replace procedural editor's inline tag input with TagInput component
  (supports autocomplete, Tab, comma, semicolon, and paste splitting)
- Add semicolon delimiter support to TagInput component

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add type-aware routing for procedural flows

Centralizes tree navigation routing via getTreeNavigatePath helper.
Fixes all pages to route procedural sessions to /flows/:id/navigate
instead of /trees/:id/navigate. Adds safety redirect in troubleshooting
navigator and resume support in procedural navigator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove unused index prop from IntakeFieldEditor

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #76.
This commit is contained in:
chihlasm
2026-02-14 23:08:17 -05:00
committed by GitHub
parent 6b4304ab92
commit 57f429f33b
32 changed files with 2199 additions and 426 deletions

View File

@@ -1,4 +1,5 @@
import { ChevronUp, AlertTriangle, Clock, ExternalLink, CheckSquare, Terminal, Type } from 'lucide-react'
import { useState } from 'react'
import { ChevronUp, ChevronDown, AlertTriangle, Clock, ExternalLink, CheckSquare, Terminal, Settings2 } from 'lucide-react'
import type { ProceduralStep, StepContentType, IntakeFormField } from '@/types'
import { cn } from '@/lib/utils'
@@ -18,6 +19,35 @@ interface StepEditorProps {
}
export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVariables }: StepEditorProps) {
const [showMore, setShowMore] = useState(false)
// Section header steps get a minimal editor
if (step.type === 'section_header') {
return (
<div className="glass-card rounded-xl border border-white/10 p-4">
<div className="mb-4 flex items-center justify-between">
<span className="text-sm font-medium text-white/50">Edit Section Header</span>
<button
onClick={onCollapse}
className="rounded p-1 text-white/40 hover:bg-white/10 hover:text-white"
>
<ChevronUp className="h-4 w-4" />
</button>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-white/50">Title</label>
<input
type="text"
value={step.title}
onChange={(e) => onUpdate({ title: e.target.value })}
placeholder="Section title"
className="w-full rounded border border-white/10 bg-black/50 px-3 py-2 text-sm text-white placeholder:text-white/30 focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20"
/>
</div>
</div>
)
}
return (
<div className="glass-card rounded-xl border border-white/10 p-4">
{/* Header */}
@@ -48,55 +78,18 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa
/>
</div>
{/* Content type + Section header row */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 block text-xs font-medium text-white/50">Content Type</label>
<div className="flex gap-1">
{CONTENT_TYPE_OPTIONS.map((opt) => (
<button
key={opt.value}
onClick={() => onUpdate({ content_type: opt.value })}
className={cn(
'rounded px-2 py-1 text-xs font-medium transition-colors',
step.content_type === opt.value
? 'bg-white/15 ' + opt.color
: 'text-white/40 hover:bg-white/10 hover:text-white/60'
)}
>
{opt.label}
</button>
))}
</div>
</div>
<div>
<label className="mb-1 flex items-center gap-1 text-xs font-medium text-white/50">
<Clock className="h-3 w-3" />
Est. Minutes
</label>
<input
type="number"
value={step.estimated_minutes || ''}
onChange={(e) => onUpdate({ estimated_minutes: e.target.value ? parseInt(e.target.value) : undefined })}
placeholder="—"
min={1}
className="w-full rounded border border-white/10 bg-black/50 px-3 py-2 text-sm text-white placeholder:text-white/30 focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20"
/>
</div>
</div>
{/* Section Header */}
<div>
{/* Est. Minutes */}
<div className="w-40">
<label className="mb-1 flex items-center gap-1 text-xs font-medium text-white/50">
<Type className="h-3 w-3" />
Section Header (optional)
<Clock className="h-3 w-3" />
Est. Minutes
</label>
<input
type="text"
value={step.section_header || ''}
onChange={(e) => onUpdate({ section_header: e.target.value || undefined })}
placeholder="e.g. Phase 2: AD Configuration"
type="number"
value={step.estimated_minutes || ''}
onChange={(e) => onUpdate({ estimated_minutes: e.target.value ? parseInt(e.target.value) : undefined })}
placeholder=""
min={1}
className="w-full rounded border border-white/10 bg-black/50 px-3 py-2 text-sm text-white placeholder:text-white/30 focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20"
/>
</div>
@@ -127,23 +120,6 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa
)}
</div>
{/* Warning text */}
{(step.content_type === 'warning' || step.warning_text) && (
<div>
<label className="mb-1 flex items-center gap-1 text-xs font-medium text-yellow-400/70">
<AlertTriangle className="h-3 w-3" />
Warning Text
</label>
<textarea
value={step.warning_text || ''}
onChange={(e) => onUpdate({ warning_text: e.target.value || undefined })}
placeholder="Caution: This will restart the service..."
rows={2}
className="w-full rounded border border-yellow-400/20 bg-yellow-400/5 px-3 py-2 text-sm text-white placeholder:text-white/30 focus:border-yellow-400/30 focus:outline-none focus:ring-1 focus:ring-yellow-400/20"
/>
</div>
)}
{/* Commands */}
<div>
<label className="mb-1 flex items-center gap-1 text-xs font-medium text-white/50">
@@ -159,74 +135,127 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa
/>
</div>
{/* Expected Outcome */}
<div>
<label className="mb-1 block text-xs font-medium text-white/50">Expected Outcome (optional)</label>
<input
type="text"
value={step.expected_outcome || ''}
onChange={(e) => onUpdate({ expected_outcome: e.target.value || undefined })}
placeholder="Server should respond with..."
className="w-full rounded border border-white/10 bg-black/50 px-3 py-2 text-sm text-white placeholder:text-white/30 focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20"
/>
</div>
{/* More Options toggle */}
<button
type="button"
onClick={() => setShowMore(!showMore)}
className="flex items-center gap-1.5 text-xs text-white/40 hover:text-white/60"
>
<Settings2 className="h-3 w-3" />
More Options
{showMore ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
</button>
{/* Verification */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 flex items-center gap-1 text-xs font-medium text-white/50">
<CheckSquare className="h-3 w-3" />
Verification Prompt (optional)
</label>
<input
type="text"
value={step.verification_prompt || ''}
onChange={(e) => onUpdate({ verification_prompt: e.target.value || undefined })}
placeholder="Confirm the role was installed"
className="w-full rounded border border-white/10 bg-black/50 px-3 py-2 text-sm text-white placeholder:text-white/30 focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20"
/>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-white/50">Verification Type</label>
<select
value={step.verification_type || ''}
onChange={(e) => onUpdate({ verification_type: e.target.value as 'checkbox' | 'text_input' || undefined })}
className="w-full rounded border border-white/10 bg-black/50 px-3 py-2 text-sm text-white focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20"
>
<option value="">None</option>
<option value="checkbox">Checkbox (confirm done)</option>
<option value="text_input">Text input (enter value)</option>
</select>
</div>
</div>
{showMore && (
<div className="space-y-4 border-t border-white/[0.06] pt-4">
{/* Content Type */}
<div>
<label className="mb-1 block text-xs font-medium text-white/50">Content Type</label>
<div className="flex gap-1">
{CONTENT_TYPE_OPTIONS.map((opt) => (
<button
key={opt.value}
onClick={() => onUpdate({ content_type: opt.value })}
className={cn(
'rounded px-2 py-1 text-xs font-medium transition-colors',
step.content_type === opt.value
? 'bg-white/15 ' + opt.color
: 'text-white/40 hover:bg-white/10 hover:text-white/60'
)}
>
{opt.label}
</button>
))}
</div>
</div>
{/* Reference URL + Notes toggle */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 flex items-center gap-1 text-xs font-medium text-white/50">
<ExternalLink className="h-3 w-3" />
Reference URL (optional)
</label>
<input
type="url"
value={step.reference_url || ''}
onChange={(e) => onUpdate({ reference_url: e.target.value || undefined })}
placeholder="https://learn.microsoft.com/..."
className="w-full rounded border border-white/10 bg-black/50 px-3 py-2 text-sm text-white placeholder:text-white/30 focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20"
/>
</div>
<div className="flex items-end pb-1">
<label className="flex items-center gap-2 text-sm text-white/60">
{/* Warning text */}
{(step.content_type === 'warning' || step.warning_text) && (
<div>
<label className="mb-1 flex items-center gap-1 text-xs font-medium text-yellow-400/70">
<AlertTriangle className="h-3 w-3" />
Warning Text
</label>
<textarea
value={step.warning_text || ''}
onChange={(e) => onUpdate({ warning_text: e.target.value || undefined })}
placeholder="Caution: This will restart the service..."
rows={2}
className="w-full rounded border border-yellow-400/20 bg-yellow-400/5 px-3 py-2 text-sm text-white placeholder:text-white/30 focus:border-yellow-400/30 focus:outline-none focus:ring-1 focus:ring-yellow-400/20"
/>
</div>
)}
{/* Expected Outcome */}
<div>
<label className="mb-1 block text-xs font-medium text-white/50">Expected Outcome (optional)</label>
<input
type="checkbox"
checked={step.notes_enabled !== false}
onChange={(e) => onUpdate({ notes_enabled: e.target.checked })}
className="rounded border-white/20"
type="text"
value={step.expected_outcome || ''}
onChange={(e) => onUpdate({ expected_outcome: e.target.value || undefined })}
placeholder="Server should respond with..."
className="w-full rounded border border-white/10 bg-black/50 px-3 py-2 text-sm text-white placeholder:text-white/30 focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20"
/>
Allow tech notes
</label>
</div>
{/* Verification */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 flex items-center gap-1 text-xs font-medium text-white/50">
<CheckSquare className="h-3 w-3" />
Verification Prompt (optional)
</label>
<input
type="text"
value={step.verification_prompt || ''}
onChange={(e) => onUpdate({ verification_prompt: e.target.value || undefined })}
placeholder="Confirm the role was installed"
className="w-full rounded border border-white/10 bg-black/50 px-3 py-2 text-sm text-white placeholder:text-white/30 focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20"
/>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-white/50">Verification Type</label>
<select
value={step.verification_type || ''}
onChange={(e) => onUpdate({ verification_type: e.target.value as 'checkbox' | 'text_input' || undefined })}
className="w-full rounded border border-white/10 bg-black/50 px-3 py-2 text-sm text-white focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20"
>
<option value="">None</option>
<option value="checkbox">Checkbox (confirm done)</option>
<option value="text_input">Text input (enter value)</option>
</select>
</div>
</div>
{/* Reference URL + Notes toggle */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 flex items-center gap-1 text-xs font-medium text-white/50">
<ExternalLink className="h-3 w-3" />
Reference URL (optional)
</label>
<input
type="url"
value={step.reference_url || ''}
onChange={(e) => onUpdate({ reference_url: e.target.value || undefined })}
placeholder="https://learn.microsoft.com/..."
className="w-full rounded border border-white/10 bg-black/50 px-3 py-2 text-sm text-white placeholder:text-white/30 focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20"
/>
</div>
<div className="flex items-end pb-1">
<label className="flex items-center gap-2 text-sm text-white/60">
<input
type="checkbox"
checked={step.notes_enabled !== false}
onChange={(e) => onUpdate({ notes_enabled: e.target.checked })}
className="rounded border-white/20"
/>
Allow tech notes
</label>
</div>
</div>
</div>
</div>
)}
</div>
</div>
)