feat: roll out illustrative empty states across 8 pages with 2 new guide entries
- TreeLibraryPage: split empty state into no-flows (illustration + CTA) vs no-filter-results - MyAnalyticsPage/TeamAnalyticsPage: add zero-sessions empty state with illustration - SessionHistoryPage: split into no-sessions (illustration) vs no-filter-results - StepLibraryBrowser: illustrative empty state when no steps exist - ScriptTemplateList: replace plain empty state with ScriptIllustration - MySharesPage: replace icon-based empty state with ShareIllustration - IntegrationsPage: add IntegrationIllustration above setup form - Add script-templates and psa-setup guides to guides data - Add EmptyState vitest tests (7 tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
92
frontend/src/components/common/__tests__/EmptyState.test.tsx
Normal file
92
frontend/src/components/common/__tests__/EmptyState.test.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { EmptyState } from '../EmptyState'
|
||||
import { FlowIllustration } from '../EmptyStateIllustrations'
|
||||
|
||||
function renderWithRouter(ui: React.ReactElement) {
|
||||
return render(<BrowserRouter>{ui}</BrowserRouter>)
|
||||
}
|
||||
|
||||
describe('EmptyState', () => {
|
||||
it('renders title and description', () => {
|
||||
renderWithRouter(
|
||||
<EmptyState
|
||||
title="No items found"
|
||||
description="Try adjusting your filters."
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByText('No items found')).toBeInTheDocument()
|
||||
expect(screen.getByText('Try adjusting your filters.')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders illustration when provided', () => {
|
||||
const { container } = renderWithRouter(
|
||||
<EmptyState
|
||||
title="Empty"
|
||||
illustration={<FlowIllustration />}
|
||||
/>
|
||||
)
|
||||
|
||||
const svg = container.querySelector('svg')
|
||||
expect(svg).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders action button', () => {
|
||||
renderWithRouter(
|
||||
<EmptyState
|
||||
title="No data"
|
||||
action={<button>Create New</button>}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Create New' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders learn more link with correct href', () => {
|
||||
renderWithRouter(
|
||||
<EmptyState
|
||||
title="Get started"
|
||||
learnMoreLink="/guides/creating-flows"
|
||||
/>
|
||||
)
|
||||
|
||||
const link = screen.getByText(/Learn more/i)
|
||||
expect(link).toBeInTheDocument()
|
||||
expect(link).toHaveAttribute('href', '/guides/creating-flows')
|
||||
})
|
||||
|
||||
it('renders custom learn more text', () => {
|
||||
renderWithRouter(
|
||||
<EmptyState
|
||||
title="Get started"
|
||||
learnMoreLink="/guides/test"
|
||||
learnMoreText="View guide"
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByText(/View guide/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders without optional props', () => {
|
||||
renderWithRouter(<EmptyState title="Just a title" />)
|
||||
|
||||
expect(screen.getByText('Just a title')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('link')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('prefers illustration over icon when both provided', () => {
|
||||
const { container } = renderWithRouter(
|
||||
<EmptyState
|
||||
title="Test"
|
||||
icon={<span data-testid="icon">icon</span>}
|
||||
illustration={<FlowIllustration />}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(container.querySelector('svg')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('icon')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
import { FileCode, Search } from 'lucide-react'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useScriptGeneratorStore } from '@/store/scriptGeneratorStore'
|
||||
import { EmptyState } from '@/components/common/EmptyState'
|
||||
import { ScriptIllustration } from '@/components/common/EmptyStateIllustrations'
|
||||
import { TemplateCard } from './TemplateCard'
|
||||
|
||||
interface Props {
|
||||
@@ -52,10 +54,13 @@ export function ScriptTemplateList({ inputValue, onClearSearch, onConfigure }: P
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-3 py-12 text-center px-4">
|
||||
<FileCode size={32} className="text-muted-foreground/40" />
|
||||
<p className="text-sm text-muted-foreground">No templates found</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
illustration={<ScriptIllustration />}
|
||||
title="Automate with script templates"
|
||||
description="Pre-built and custom scripts your team can reference during sessions. PowerShell, bash, and more."
|
||||
learnMoreLink="/guides/script-templates"
|
||||
className="px-4"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { Search, ChevronDown, ChevronUp, Loader2 } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { EmptyState } from '@/components/common/EmptyState'
|
||||
import { StepLibraryIllustration } from '@/components/common/EmptyStateIllustrations'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { stepsApi } from '@/api/steps'
|
||||
import { stepCategoriesApi } from '@/api/stepCategories'
|
||||
@@ -259,12 +261,24 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
</Button>
|
||||
</div>
|
||||
) : steps.length === 0 ? (
|
||||
<div className="rounded-lg border border-border bg-accent/50 p-12 text-center">
|
||||
<p className="mb-2 text-lg font-medium text-foreground">No steps found</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{hasActiveFilters ? 'Try adjusting your filters' : 'Create your first step to get started!'}
|
||||
</p>
|
||||
</div>
|
||||
hasActiveFilters ? (
|
||||
<div className="rounded-lg border border-border bg-accent/50 p-12 text-center">
|
||||
<p className="mb-2 text-lg font-medium text-foreground">No steps found</p>
|
||||
<p className="text-sm text-muted-foreground">Try adjusting your filters</p>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
illustration={<StepLibraryIllustration />}
|
||||
title="Build a reusable step library"
|
||||
description="Save common troubleshooting steps once, reuse them across flows. Keeps your team consistent and saves build time."
|
||||
action={
|
||||
onCreateNew ? (
|
||||
<Button onClick={onCreateNew}>Browse Steps</Button>
|
||||
) : undefined
|
||||
}
|
||||
learnMoreLink="/guides/step-library"
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* My Steps */}
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
Wrench,
|
||||
Settings,
|
||||
BarChart3,
|
||||
Terminal,
|
||||
Plug,
|
||||
} from 'lucide-react'
|
||||
|
||||
export interface GuideStep {
|
||||
@@ -492,4 +494,82 @@ export const guides: Guide[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'script-templates',
|
||||
title: 'Script Templates',
|
||||
icon: Terminal,
|
||||
summary: 'Browse, configure, and generate scripts from reusable templates.',
|
||||
sections: [
|
||||
{
|
||||
title: 'Browsing Templates',
|
||||
steps: [
|
||||
{ instruction: 'Click **Scripts** in the sidebar to open the Script Library.' },
|
||||
{ instruction: 'The left pane lists all available templates organized by category.' },
|
||||
{ instruction: 'Use the search bar to filter templates by name or keyword.' },
|
||||
{ instruction: 'Click any template to preview its script content in the right pane.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Configuring and Generating Scripts',
|
||||
steps: [
|
||||
{ instruction: 'Click **Configure** on a template to enter parameter values.' },
|
||||
{ instruction: 'Fill in the required fields (e.g., server name, IP address, credentials).' },
|
||||
{ instruction: 'Click **Generate** to produce a ready-to-run script with your values substituted.' },
|
||||
{ instruction: 'Copy the generated script to your clipboard or download it directly.', tip: 'Double-check generated scripts in a test environment before running them in production.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Managing Templates',
|
||||
steps: [
|
||||
{ instruction: 'Click **Manage Templates** at the top of the Script Library page.' },
|
||||
{ instruction: 'Create new templates with a name, category, script body, and configurable parameters.' },
|
||||
{ instruction: 'Edit or delete existing templates from the management page.' },
|
||||
{ instruction: 'Templates support PowerShell, Bash, Python, and other scripting languages.' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'psa-setup',
|
||||
title: 'PSA Integration Setup',
|
||||
icon: Plug,
|
||||
summary: 'Connect ConnectWise or other PSA tools to ResolutionFlow.',
|
||||
sections: [
|
||||
{
|
||||
title: 'Getting Your API Credentials',
|
||||
steps: [
|
||||
{ instruction: 'Log in to your ConnectWise PSA instance as an admin.' },
|
||||
{ instruction: 'Navigate to **System > Members > API Members** and create a new API member.' },
|
||||
{ instruction: 'Generate an **API key pair** (public key and private key) for the member.' },
|
||||
{ instruction: 'Note your **Company ID** (the company identifier used to log in) and **Site URL** (e.g., na.myconnectwise.net).', tip: 'Create a dedicated API member for ResolutionFlow with minimal permissions for security.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Connecting in ResolutionFlow',
|
||||
steps: [
|
||||
{ instruction: 'Go to **Account > Integrations** in ResolutionFlow.' },
|
||||
{ instruction: 'Enter a display name, your Site URL, Company ID, Public Key, and Private Key.' },
|
||||
{ instruction: 'Click **Connect** to save the connection.' },
|
||||
{ instruction: 'Click **Test Connection** to verify everything is working correctly.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Member Mapping',
|
||||
steps: [
|
||||
{ instruction: 'After connecting, switch to the **Member Mapping** tab.' },
|
||||
{ instruction: 'Click **Auto-Match by Email** to automatically pair ResolutionFlow users with ConnectWise members by email address.' },
|
||||
{ instruction: 'Manually adjust any unmatched or incorrectly matched members using the dropdowns.' },
|
||||
{ instruction: 'Click **Save Mappings** to apply changes. Mapped members are used when posting session notes to tickets.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'What the Integration Enables',
|
||||
steps: [
|
||||
{ instruction: 'Session documentation can be posted directly to ConnectWise tickets as internal notes.' },
|
||||
{ instruction: 'Ticket context (client info, issue details) can be pulled into sessions for AI-assisted troubleshooting.' },
|
||||
{ instruction: 'Posts are attributed to the correct ConnectWise member based on your member mappings.' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from 'recharts'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { EmptyState } from '@/components/common/EmptyState'
|
||||
import { AnalyticsIllustration } from '@/components/common/EmptyStateIllustrations'
|
||||
import { analyticsApi } from '@/api'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
import type { PersonalAnalyticsResponse, AnalyticsPeriod } from '@/types'
|
||||
@@ -57,6 +58,7 @@ export default function MyAnalyticsPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<EmptyState
|
||||
illustration={<AnalyticsIllustration />}
|
||||
title="Analytics unavailable"
|
||||
description="Failed to load analytics data. Please try again."
|
||||
/>
|
||||
@@ -64,6 +66,27 @@ export default function MyAnalyticsPage() {
|
||||
)
|
||||
}
|
||||
|
||||
if (data.summary.total_sessions === 0) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<EmptyState
|
||||
illustration={<AnalyticsIllustration />}
|
||||
title="Track your troubleshooting performance"
|
||||
description="Analytics show resolution times, common paths, and team efficiency. Data appears automatically as you complete sessions."
|
||||
action={
|
||||
<Link
|
||||
to="/trees"
|
||||
className="inline-flex items-center gap-2 rounded-[10px] bg-gradient-brand px-5 py-2.5 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] transition-all"
|
||||
>
|
||||
Run Your First Session
|
||||
</Link>
|
||||
}
|
||||
learnMoreLink="/guides/analytics"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { summary, time_series, top_flows } = data
|
||||
const outcomeBreakdown = summary.outcome_breakdown
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { Globe, Users, Copy, Check, Link2, ExternalLink, Trash2, ArrowLeft } from 'lucide-react'
|
||||
import { Globe, Users, Copy, Check, ExternalLink, Trash2, ArrowLeft } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { EmptyState } from '@/components/common/EmptyState'
|
||||
import { ShareIllustration } from '@/components/common/EmptyStateIllustrations'
|
||||
import { ConfirmDialog } from '@/components/common/ConfirmDialog'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from '@/lib/toast'
|
||||
@@ -142,18 +143,17 @@ export default function MySharesPage() {
|
||||
|
||||
{/* Empty state */}
|
||||
{shares.length === 0 ? (
|
||||
<div className="bg-card border border-border rounded-xl">
|
||||
<EmptyState
|
||||
icon={<Link2 className="h-12 w-12" />}
|
||||
title="No shared sessions"
|
||||
description="Share a session from the session detail page to create a link"
|
||||
action={
|
||||
<Button onClick={() => navigate('/sessions')}>
|
||||
Go to Sessions
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<EmptyState
|
||||
illustration={<ShareIllustration />}
|
||||
title="Share session results with your team"
|
||||
description="Create shareable links to completed sessions for knowledge sharing and client communication."
|
||||
action={
|
||||
<Button onClick={() => navigate('/sessions')}>
|
||||
View Sessions
|
||||
</Button>
|
||||
}
|
||||
learnMoreLink="/guides/sharing-exports"
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{shares.map((share) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { sessionsApi } from '@/api/sessions'
|
||||
import { treesApi } from '@/api/trees'
|
||||
@@ -9,6 +9,7 @@ import { SessionFilters } from '@/components/session/SessionFilters'
|
||||
import type { SessionFilterState } from '@/components/session/SessionFilters'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { EmptyState } from '@/components/common/EmptyState'
|
||||
import { SessionIllustration } from '@/components/common/EmptyStateIllustrations'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { getSessionResumePath } from '@/lib/routing'
|
||||
@@ -259,19 +260,32 @@ export function SessionHistoryPage() {
|
||||
<Spinner />
|
||||
</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<EmptyState
|
||||
title="No sessions found"
|
||||
description={filters.ticketNumber || filters.clientName || filters.treeName || filters.dateRange?.from
|
||||
? "Try adjusting your filters"
|
||||
: "Complete a flow to see it here"}
|
||||
action={
|
||||
(filters.ticketNumber || filters.clientName || filters.treeName || filters.dateRange?.from) ? (
|
||||
(filters.ticketNumber || filters.clientName || filters.treeName || filters.dateRange?.from) ? (
|
||||
<EmptyState
|
||||
title="No sessions match your filters"
|
||||
description="Try adjusting your search or filters."
|
||||
action={
|
||||
<button onClick={handleClearFilters} className="text-foreground hover:underline text-sm">
|
||||
Clear all filters
|
||||
</button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
illustration={<SessionIllustration />}
|
||||
title="Your session history will appear here"
|
||||
description="Every troubleshooting session is recorded with decisions, timing, and outcomes — ready for export or review."
|
||||
action={
|
||||
<Link
|
||||
to="/trees"
|
||||
className="inline-flex items-center gap-2 rounded-[10px] bg-gradient-brand px-5 py-2.5 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] transition-all"
|
||||
>
|
||||
Start a Session
|
||||
</Link>
|
||||
}
|
||||
learnMoreLink="/guides/sessions"
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from 'recharts'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { EmptyState } from '@/components/common/EmptyState'
|
||||
import { AnalyticsIllustration } from '@/components/common/EmptyStateIllustrations'
|
||||
import { analyticsApi } from '@/api'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
import { toast } from '@/lib/toast'
|
||||
@@ -70,6 +71,7 @@ export default function TeamAnalyticsPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<EmptyState
|
||||
illustration={<AnalyticsIllustration />}
|
||||
title="Analytics unavailable"
|
||||
description="Failed to load analytics data. Please try again."
|
||||
/>
|
||||
@@ -77,6 +79,27 @@ export default function TeamAnalyticsPage() {
|
||||
)
|
||||
}
|
||||
|
||||
if (data.summary.total_sessions === 0) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<EmptyState
|
||||
illustration={<AnalyticsIllustration />}
|
||||
title="Track your troubleshooting performance"
|
||||
description="Analytics show resolution times, common paths, and team efficiency. Data appears automatically as you complete sessions."
|
||||
action={
|
||||
<Link
|
||||
to="/trees"
|
||||
className="inline-flex items-center gap-2 rounded-[10px] bg-gradient-brand px-5 py-2.5 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] transition-all"
|
||||
>
|
||||
Run Your First Session
|
||||
</Link>
|
||||
}
|
||||
learnMoreLink="/guides/analytics"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { summary, time_series, top_flows, top_engineers } = data
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { X, RotateCcw, Play, FileUp } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { FlowIllustration } from '@/components/common/EmptyStateIllustrations'
|
||||
import { treesApi } from '@/api/trees'
|
||||
import { categoriesApi } from '@/api/categories'
|
||||
import { foldersApi } from '@/api/folders'
|
||||
@@ -496,14 +497,29 @@ export function TreeLibraryPage() {
|
||||
<Spinner />
|
||||
</div>
|
||||
) : trees.length === 0 ? (
|
||||
<EmptyState
|
||||
title="No flows found"
|
||||
description={
|
||||
(searchQuery || hasActiveFilters)
|
||||
? 'Try adjusting your filters.'
|
||||
: 'Create your first flow to get started.'
|
||||
}
|
||||
/>
|
||||
(searchQuery || hasActiveFilters) ? (
|
||||
<EmptyState
|
||||
title="No flows match your filters"
|
||||
description="Try adjusting your search or filters."
|
||||
action={
|
||||
<Button variant="secondary" onClick={clearAllFilters}>
|
||||
Clear Filters
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
illustration={<FlowIllustration />}
|
||||
title="Build your first troubleshooting flow"
|
||||
description="Flows guide your team through proven resolution paths, capturing every decision along the way."
|
||||
action={
|
||||
canCreateTrees ? (
|
||||
<CreateFlowDropdown aiEnabled={aiEnabled} label="Create a Flow" />
|
||||
) : undefined
|
||||
}
|
||||
learnMoreLink="/guides/creating-flows"
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{treeLibraryView === 'grid' && (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Plug, CheckCircle2, AlertCircle, Loader2, Pencil, Trash2, Shield, History, Ticket, Users, Zap, Save } from 'lucide-react'
|
||||
import { analytics } from '@/lib/analytics'
|
||||
import { EmptyState } from '@/components/common/EmptyState'
|
||||
import { IntegrationIllustration } from '@/components/common/EmptyStateIllustrations'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { integrationsApi } from '@/api/integrations'
|
||||
import type { PsaConnectionResponse, PsaConnectionCreate, PsaConnectionUpdate, PsaConnectionTestResponse } from '@/types'
|
||||
@@ -254,6 +256,18 @@ export function IntegrationsPage() {
|
||||
{/* Connection Tab */}
|
||||
{activeTab === 'connection' && (
|
||||
<div className="max-w-3xl">
|
||||
{/* Illustrative empty state when no connection exists */}
|
||||
{mode === 'setup' && (
|
||||
<div className="mb-6">
|
||||
<EmptyState
|
||||
illustration={<IntegrationIllustration />}
|
||||
title="Connect your PSA for seamless workflows"
|
||||
description="Link ConnectWise or other PSA tools to pull ticket context into sessions and push documentation back automatically."
|
||||
learnMoreLink="/guides/psa-setup"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Setup / Edit Form */}
|
||||
{(mode === 'setup' || mode === 'edit') && (
|
||||
<div className="glass-card-static p-6">
|
||||
|
||||
Reference in New Issue
Block a user