From dfdc6cae9cd22bab4a450f77665d987243451d61 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Tue, 17 Mar 2026 01:21:11 -0400 Subject: [PATCH] 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) --- .../common/__tests__/EmptyState.test.tsx | 92 +++++++++++++++++++ .../components/scripts/ScriptTemplateList.tsx | 15 ++- .../step-library/StepLibraryBrowser.tsx | 26 ++++-- frontend/src/data/guides.ts | 80 ++++++++++++++++ frontend/src/pages/MyAnalyticsPage.tsx | 23 +++++ frontend/src/pages/MySharesPage.tsx | 26 +++--- frontend/src/pages/SessionHistoryPage.tsx | 36 +++++--- frontend/src/pages/TeamAnalyticsPage.tsx | 23 +++++ frontend/src/pages/TreeLibraryPage.tsx | 32 +++++-- .../src/pages/account/IntegrationsPage.tsx | 14 +++ 10 files changed, 324 insertions(+), 43 deletions(-) create mode 100644 frontend/src/components/common/__tests__/EmptyState.test.tsx diff --git a/frontend/src/components/common/__tests__/EmptyState.test.tsx b/frontend/src/components/common/__tests__/EmptyState.test.tsx new file mode 100644 index 00000000..83b81a3c --- /dev/null +++ b/frontend/src/components/common/__tests__/EmptyState.test.tsx @@ -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({ui}) +} + +describe('EmptyState', () => { + it('renders title and description', () => { + renderWithRouter( + + ) + + expect(screen.getByText('No items found')).toBeInTheDocument() + expect(screen.getByText('Try adjusting your filters.')).toBeInTheDocument() + }) + + it('renders illustration when provided', () => { + const { container } = renderWithRouter( + } + /> + ) + + const svg = container.querySelector('svg') + expect(svg).toBeInTheDocument() + }) + + it('renders action button', () => { + renderWithRouter( + Create New} + /> + ) + + expect(screen.getByRole('button', { name: 'Create New' })).toBeInTheDocument() + }) + + it('renders learn more link with correct href', () => { + renderWithRouter( + + ) + + const link = screen.getByText(/Learn more/i) + expect(link).toBeInTheDocument() + expect(link).toHaveAttribute('href', '/guides/creating-flows') + }) + + it('renders custom learn more text', () => { + renderWithRouter( + + ) + + expect(screen.getByText(/View guide/i)).toBeInTheDocument() + }) + + it('renders without optional props', () => { + renderWithRouter() + + 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( + icon} + illustration={} + /> + ) + + expect(container.querySelector('svg')).toBeInTheDocument() + expect(screen.queryByTestId('icon')).not.toBeInTheDocument() + }) +}) diff --git a/frontend/src/components/scripts/ScriptTemplateList.tsx b/frontend/src/components/scripts/ScriptTemplateList.tsx index e0f32476..f1dc21da 100644 --- a/frontend/src/components/scripts/ScriptTemplateList.tsx +++ b/frontend/src/components/scripts/ScriptTemplateList.tsx @@ -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 ( -
- -

No templates found

-
+ } + 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" + /> ) } diff --git a/frontend/src/components/step-library/StepLibraryBrowser.tsx b/frontend/src/components/step-library/StepLibraryBrowser.tsx index 3f28805e..68787f42 100644 --- a/frontend/src/components/step-library/StepLibraryBrowser.tsx +++ b/frontend/src/components/step-library/StepLibraryBrowser.tsx @@ -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 ) : steps.length === 0 ? ( -
-

No steps found

-

- {hasActiveFilters ? 'Try adjusting your filters' : 'Create your first step to get started!'} -

-
+ hasActiveFilters ? ( +
+

No steps found

+

Try adjusting your filters

+
+ ) : ( + } + 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 ? ( + + ) : undefined + } + learnMoreLink="/guides/step-library" + /> + ) ) : (
{/* My Steps */} diff --git a/frontend/src/data/guides.ts b/frontend/src/data/guides.ts index 68f596b1..413c8192 100644 --- a/frontend/src/data/guides.ts +++ b/frontend/src/data/guides.ts @@ -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.' }, + ], + }, + ], + }, ] diff --git a/frontend/src/pages/MyAnalyticsPage.tsx b/frontend/src/pages/MyAnalyticsPage.tsx index 28460d55..dfa92ac3 100644 --- a/frontend/src/pages/MyAnalyticsPage.tsx +++ b/frontend/src/pages/MyAnalyticsPage.tsx @@ -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 (
} 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 ( +
+ } + title="Track your troubleshooting performance" + description="Analytics show resolution times, common paths, and team efficiency. Data appears automatically as you complete sessions." + action={ + + Run Your First Session + + } + learnMoreLink="/guides/analytics" + /> +
+ ) + } + const { summary, time_series, top_flows } = data const outcomeBreakdown = summary.outcome_breakdown diff --git a/frontend/src/pages/MySharesPage.tsx b/frontend/src/pages/MySharesPage.tsx index 373ae67d..6feb4fdd 100644 --- a/frontend/src/pages/MySharesPage.tsx +++ b/frontend/src/pages/MySharesPage.tsx @@ -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 ? ( -
- } - title="No shared sessions" - description="Share a session from the session detail page to create a link" - action={ - - } - /> -
+ } + title="Share session results with your team" + description="Create shareable links to completed sessions for knowledge sharing and client communication." + action={ + + } + learnMoreLink="/guides/sharing-exports" + /> ) : (
{shares.map((share) => { diff --git a/frontend/src/pages/SessionHistoryPage.tsx b/frontend/src/pages/SessionHistoryPage.tsx index 49f89560..9a85fb8a 100644 --- a/frontend/src/pages/SessionHistoryPage.tsx +++ b/frontend/src/pages/SessionHistoryPage.tsx @@ -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() {
) : sessions.length === 0 ? ( - Clear all filters - ) : undefined - } - /> + } + /> + ) : ( + } + title="Your session history will appear here" + description="Every troubleshooting session is recorded with decisions, timing, and outcomes — ready for export or review." + action={ + + Start a Session + + } + learnMoreLink="/guides/sessions" + /> + ) ) : ( <>
diff --git a/frontend/src/pages/TeamAnalyticsPage.tsx b/frontend/src/pages/TeamAnalyticsPage.tsx index 48fa4c32..41fc67e6 100644 --- a/frontend/src/pages/TeamAnalyticsPage.tsx +++ b/frontend/src/pages/TeamAnalyticsPage.tsx @@ -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 (
} 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 ( +
+ } + title="Track your troubleshooting performance" + description="Analytics show resolution times, common paths, and team efficiency. Data appears automatically as you complete sessions." + action={ + + Run Your First Session + + } + learnMoreLink="/guides/analytics" + /> +
+ ) + } + const { summary, time_series, top_flows, top_engineers } = data return ( diff --git a/frontend/src/pages/TreeLibraryPage.tsx b/frontend/src/pages/TreeLibraryPage.tsx index cb2c5aec..9bafdc97 100644 --- a/frontend/src/pages/TreeLibraryPage.tsx +++ b/frontend/src/pages/TreeLibraryPage.tsx @@ -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() {
) : trees.length === 0 ? ( - + (searchQuery || hasActiveFilters) ? ( + + Clear Filters + + } + /> + ) : ( + } + title="Build your first troubleshooting flow" + description="Flows guide your team through proven resolution paths, capturing every decision along the way." + action={ + canCreateTrees ? ( + + ) : undefined + } + learnMoreLink="/guides/creating-flows" + /> + ) ) : ( <> {treeLibraryView === 'grid' && ( diff --git a/frontend/src/pages/account/IntegrationsPage.tsx b/frontend/src/pages/account/IntegrationsPage.tsx index 7c8d59f1..d0656079 100644 --- a/frontend/src/pages/account/IntegrationsPage.tsx +++ b/frontend/src/pages/account/IntegrationsPage.tsx @@ -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' && (
+ {/* Illustrative empty state when no connection exists */} + {mode === 'setup' && ( +
+ } + 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" + /> +
+ )} + {/* Setup / Edit Form */} {(mode === 'setup' || mode === 'edit') && (