* 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>
73 lines
2.1 KiB
TypeScript
73 lines
2.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { sessionsApi } from './sessions'
|
|
import apiClient from './client'
|
|
|
|
vi.mock('./client', () => ({
|
|
default: {
|
|
get: vi.fn(),
|
|
post: vi.fn(),
|
|
put: vi.fn(),
|
|
delete: vi.fn(),
|
|
patch: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
const mockClient = apiClient as unknown as {
|
|
get: ReturnType<typeof vi.fn>
|
|
post: ReturnType<typeof vi.fn>
|
|
delete: ReturnType<typeof vi.fn>
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('sessionsApi sharing methods', () => {
|
|
it('createShare hits POST /sessions/{id}/shares with correct payload', async () => {
|
|
const mockShare = { id: 'share-1', share_token: 'tok-123', visibility: 'public' }
|
|
mockClient.post.mockResolvedValue({ data: mockShare })
|
|
|
|
const payload = { visibility: 'public' as const, share_name: 'My Share' }
|
|
const result = await sessionsApi.createShare('session-42', payload)
|
|
|
|
expect(mockClient.post).toHaveBeenCalledWith('/sessions/session-42/shares', payload)
|
|
expect(result).toEqual(mockShare)
|
|
})
|
|
|
|
it('listMyShares hits GET /shares/my-shares', async () => {
|
|
const mockShares = [
|
|
{ id: 'share-1', share_token: 'tok-1' },
|
|
{ id: 'share-2', share_token: 'tok-2' },
|
|
]
|
|
mockClient.get.mockResolvedValue({ data: mockShares })
|
|
|
|
const result = await sessionsApi.listMyShares()
|
|
|
|
expect(mockClient.get).toHaveBeenCalledWith('/shares/my-shares')
|
|
expect(result).toEqual(mockShares)
|
|
})
|
|
|
|
it('revokeShare hits DELETE /shares/{id}', async () => {
|
|
mockClient.delete.mockResolvedValue({})
|
|
|
|
await sessionsApi.revokeShare('share-99')
|
|
|
|
expect(mockClient.delete).toHaveBeenCalledWith('/shares/share-99')
|
|
})
|
|
|
|
it('getSharedSession hits GET /share/{token}', async () => {
|
|
const mockView = {
|
|
session_id: 'sess-1',
|
|
tree_name: 'DNS Troubleshooting',
|
|
path_taken: ['root', 'node-1'],
|
|
decisions: [],
|
|
}
|
|
mockClient.get.mockResolvedValue({ data: mockView })
|
|
|
|
const result = await sessionsApi.getSharedSession('tok-abc')
|
|
|
|
expect(mockClient.get).toHaveBeenCalledWith('/share/tok-abc')
|
|
expect(result).toEqual(mockView)
|
|
})
|
|
})
|