@graph8/sdk 0.362.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/blocks/_foundation/README.md +78 -1
- package/blocks/_foundation/block.json +123 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +2 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +93 -2
- package/blocks/agents/block.json +343 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +29 -10
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +119 -0
- package/blocks/agents/components/agent-runs.tsx +19 -10
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +226 -0
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +222 -0
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +38 -11
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +33 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +38 -18
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +17 -6
- package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +163 -0
- package/blocks/campaigns/components/icp-editor.tsx +22 -17
- package/blocks/campaigns/components/persona-detail.tsx +181 -0
- package/blocks/campaigns/components/persona-editor.tsx +19 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
- package/blocks/crm/README.md +34 -0
- package/blocks/crm/block.json +107 -2
- package/blocks/crm/components/companies-screen.tsx +359 -0
- package/blocks/crm/components/company-detail.tsx +10 -8
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +10 -7
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +377 -0
- package/blocks/crm/components/dedupe-review.tsx +13 -6
- package/blocks/crm/components/import-wizard.tsx +7 -9
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +6 -4
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +399 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +9 -1
- package/blocks/crm/lib/crm-types.ts +26 -2
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +160 -5
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +25 -13
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +31 -10
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +66 -26
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +5 -5
- package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +45 -4
- package/blocks/dialer/block.json +84 -3
- package/blocks/dialer/components/call-contact-button.tsx +269 -0
- package/blocks/dialer/components/call-log-screen.tsx +208 -0
- package/blocks/dialer/components/call-log.tsx +23 -7
- package/blocks/dialer/components/call-queue.tsx +4 -4
- package/blocks/dialer/components/dialer-screen.tsx +361 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +61 -12
- package/blocks/dialer/components/softphone.tsx +4 -2
- package/blocks/enrichment/README.md +29 -7
- package/blocks/enrichment/block.json +49 -9
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
- package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
- package/blocks/enrichment/components/waterfall-config.tsx +66 -17
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +139 -18
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +4 -2
- package/blocks/forms/components/form-embed.tsx +4 -2
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +56 -19
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +17 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +32 -3
- package/blocks/inbox/block.json +29 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +63 -1
- package/blocks/integrations/block.json +234 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +119 -0
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +13 -8
- package/blocks/integrations/components/integrations-screen.tsx +152 -0
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +11 -9
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +179 -0
- package/blocks/integrations/components/webhook-manager.tsx +43 -25
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +11 -1
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +13 -10
- package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/block.json +13 -8
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +20 -1
- package/blocks/lists/block.json +69 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +20 -9
- package/blocks/lists/components/list-picker.tsx +17 -3
- package/blocks/lists/components/list-table.tsx +12 -6
- package/blocks/lists/components/lists-screen.tsx +346 -0
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +33 -8
- package/blocks/lists/components/suppressions-screen.tsx +264 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +69 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
- package/blocks/mailboxes/components/domain-detail.tsx +148 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
- package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
- package/blocks/mailboxes/components/warmup-health.tsx +24 -22
- package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +157 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +13 -3
- package/blocks/nurture/components/nurture-enroll.tsx +12 -10
- package/blocks/nurture/components/nurture-form.tsx +27 -10
- package/blocks/nurture/components/nurture-screen.tsx +26 -11
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +18 -1
- package/blocks/objects/block.json +42 -21
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +10 -2
- package/blocks/objects/components/object-schema-editor.tsx +4 -2
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +140 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +65 -13
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +8 -4
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +55 -9
- package/blocks/sequencer/block.json +126 -19
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +224 -60
- package/blocks/sequencer/components/sequence-detail.tsx +261 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
- package/blocks/sequencer/components/sequence-list.tsx +34 -9
- package/blocks/sequencer/components/sequence-stats.tsx +11 -10
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +162 -0
- package/blocks/sequencer/components/step-editor.tsx +14 -12
- package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +19 -0
- package/blocks/social-ads/block.json +233 -1
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
- package/blocks/social-ads/components/audience-sync.tsx +17 -9
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +138 -0
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +155 -0
- package/blocks/social-ads/components/creative-gallery.tsx +35 -7
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +7 -3
- package/blocks/social-ads/components/post-detail.tsx +163 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +187 -0
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +21 -10
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +26 -16
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +16 -7
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +8 -5
- package/blocks/webchat/components/conversation-list.tsx +8 -4
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +10 -4
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +202 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +4 -2
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +3109 -1977
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +122 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +27 -20
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +55 -17
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +147 -37
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +64 -0
- package/stacks/clay/pages/companies.tsx.tmpl +31 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +73 -27
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +98 -35
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +104 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +181 -64
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +52 -20
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +68 -34
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +106 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +117 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +85 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +115 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
package/blocks/inbox/README.md
CHANGED
|
@@ -7,6 +7,23 @@ copy `InboxScreen`, the finished two-pane page.
|
|
|
7
7
|
Depends on: `crm` (the contact on each thread), `mailboxes` (the connected sending mailboxes). Install with
|
|
8
8
|
`npx @graph8/sdk add inbox`.
|
|
9
9
|
|
|
10
|
+
## Screens
|
|
11
|
+
|
|
12
|
+
`InboxScreen` (`components/inbox-screen.tsx`) is the finished page; it needs no props. Stat strip, channel / status /
|
|
13
|
+
tag filters, search of the loaded page, paged list, and the open conversation (reply composer with confirmed AI draft
|
|
14
|
+
and send, tag and assign). Open conversations deep-link as `?id=<channel>:<thread id>`. Optional slots:
|
|
15
|
+
`toolbarActions`, `renderRowActions(thread)`, `renderDetailActions(thread)`, `extraDetailTabs`, `pageSize`.
|
|
16
|
+
Needs scopes `inbox:read`, `inbox:write`, `inbox:run` (plus `campaigns:read` for the SMS sender picker) and a mailbox
|
|
17
|
+
connected in the graph8 app (a link-out card says so).
|
|
18
|
+
|
|
19
|
+
**Behaviour change.** Compared with the original two-pane `InboxScreen`, the finished page now writes `?id=` and
|
|
20
|
+
`?view=` to the host page URL and renders its own header (an "Inbox" h1, description and stats). If your page
|
|
21
|
+
already has a title or owns those query keys, pass `urlState={false}` (the open conversation lives in component
|
|
22
|
+
state, the URL is untouched) and/or `hideHeader` (no title or stats, just search, filters, Refresh and the two panes).
|
|
23
|
+
Defaults are unchanged from this release: both are on.
|
|
24
|
+
|
|
25
|
+
Every detail opens as a full page with `&view=full` (the Open full page button).
|
|
26
|
+
|
|
10
27
|
## The flow, in order
|
|
11
28
|
|
|
12
29
|
1. **List** `GET /inbox?channel=&status=&page=&page_size=` (`InboxList`). Each row has `id` and `channel`.
|
|
@@ -18,8 +35,12 @@ Depends on: `crm` (the contact on each thread), `mailboxes` (the connected sendi
|
|
|
18
35
|
`shorten` / `expand` / `fix_grammar_spelling` rework the text you pass in `message_html`. The response
|
|
19
36
|
`data.content` is HTML and `data.plain_content` is text. It sends nothing.
|
|
20
37
|
4. **Send** `POST /inbox/{reply_id}/send` (`ReplyComposer`, **external**, confirm first) with
|
|
21
|
-
`{ channel, body, subject?, to
|
|
22
|
-
|
|
38
|
+
`{ channel, body, subject?, to, from_address }`. `from_address` is the thread's own sender: `thread.mailbox`
|
|
39
|
+
(email), `thread.from_number` (SMS) or `thread.linkedin_account_id` (LinkedIn); `to` is the thread's contact
|
|
40
|
+
(`contact.email`, or `thread.to_number` for SMS). If an SMS or LinkedIn thread names no sender, the composer
|
|
41
|
+
shows a picker filled from `GET /sms/phone-numbers` or `GET /inbox/linkedin/accounts`. The API fills both
|
|
42
|
+
from the thread when omitted (older API versions refused the send with 400). The composer sends an
|
|
43
|
+
`Idempotency-Key` so a double click cannot send twice.
|
|
23
44
|
5. **Route** (`TagAndAssign`): `POST /inbox/tags` creates a tag (write), `POST /inbox/{id}/tag?channel=`
|
|
24
45
|
applies tag ids and `POST /inbox/{id}/assign?channel=` hands the thread to a teammate by email. Both of
|
|
25
46
|
those are **external** (confirm first) because they can fire automations and notify people.
|
|
@@ -31,13 +52,14 @@ IDs passed between steps: `thread.id` + `thread.channel` (list -> view -> draft/
|
|
|
31
52
|
|
|
32
53
|
- Connect a mailbox in the graph8 app (Google / Microsoft OAuth exists only there; see the `mailboxes` block).
|
|
33
54
|
Until then the inbox has no email threads. `where: "app"`.
|
|
34
|
-
- Give the proxy's API key `inbox:read`, `inbox:write` and `inbox:run
|
|
55
|
+
- Give the proxy's API key `inbox:read`, `inbox:write` and `inbox:run` (and `campaigns:read` for the SMS sender picker).
|
|
35
56
|
- Credits for AI drafts (402 when none; there is no balance read, so the confirm dialog shows no estimate).
|
|
36
57
|
|
|
37
58
|
## Defaults (the agent does not need to ask)
|
|
38
59
|
|
|
39
60
|
- AI drafts are only ever written into the reply box. Sending is always a separate, confirmed click.
|
|
40
61
|
- Tone `professional`, goal `follow_up`.
|
|
62
|
+
- A reply leaves from the thread's own mailbox, number or LinkedIn account and goes to the thread's contact.
|
|
41
63
|
- No assignee filter: the API cannot filter by an arbitrary assignee email (an org key owns no threads), so
|
|
42
64
|
filter `thread.assignees` client-side if you need it.
|
|
43
65
|
- Ask the human only about **tags**: there is no list-tags operation. Pass `availableTags` from your own
|
|
@@ -60,6 +82,8 @@ IDs passed between steps: `thread.id` + `thread.channel` (list -> view -> draft/
|
|
|
60
82
|
| `inbox.getReply` | GET /inbox/{reply_id} | read | ThreadView, useInboxThread |
|
|
61
83
|
| `aiInbox.generateInboxDraft` | POST /inbox/drafts/generate | billable | ReplyComposer |
|
|
62
84
|
| `inbox.sendReply` | POST /inbox/{reply_id}/send | external | ReplyComposer |
|
|
85
|
+
| `channels.listSmsPhoneNumbers` | GET /sms/phone-numbers | read | ReplyComposer (SMS sender picker) |
|
|
86
|
+
| `aiInbox.listInboxLinkedinAccounts` | GET /inbox/linkedin/accounts | read | ReplyComposer (LinkedIn sender picker) |
|
|
63
87
|
| `aiInbox.listInboxUsers` | GET /inbox/users | read | TagAndAssign |
|
|
64
88
|
| `aiInbox.createInboxTag` | POST /inbox/tags | write | TagAndAssign |
|
|
65
89
|
| `inbox.tagReply` | POST /inbox/{reply_id}/tag | external | TagAndAssign |
|
|
@@ -70,3 +94,8 @@ IDs passed between steps: `thread.id` + `thread.channel` (list -> view -> draft/
|
|
|
70
94
|
- `01-triage-and-reply.ts`: list, read, AI draft (billable), send (external).
|
|
71
95
|
- `02-tag-and-assign.ts`: create a tag, apply it, assign a teammate.
|
|
72
96
|
- `03-hot-contacts-digest.ts`: read-only digest of engaging contacts and unowned threads.
|
|
97
|
+
|
|
98
|
+
## Cross-block glue
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
- `RecordConversationsTab` (`components/record-conversations-tab.tsx`): the email conversations with a contact. The inbox has no filter by contact id, so it matches the contact's email through `searchInboxEmails` (`search_emails`) across the mailboxes from `listInboxMailboxes`, 10 per page with "Load more"; opening a row reads `getReply`, and the API's uniform 404 for a hidden thread shows "not available to you" (no retry). `extraDetailTabs` entry for ContactsScreen.
|
package/blocks/inbox/block.json
CHANGED
|
@@ -20,7 +20,11 @@
|
|
|
20
20
|
"generate_inbox_draft_inbox_drafts_generate_post",
|
|
21
21
|
"get_reply_inbox__reply_id__get",
|
|
22
22
|
"list_inbox_inbox_get",
|
|
23
|
+
"list_inbox_linkedin_accounts_inbox_linkedin_accounts_get",
|
|
24
|
+
"list_inbox_mailboxes_inbox_mailboxes_get",
|
|
23
25
|
"list_inbox_users_inbox_users_get",
|
|
26
|
+
"list_sms_phone_numbers_sms_phone_numbers_get",
|
|
27
|
+
"search_inbox_emails_inbox_emails_search_post",
|
|
24
28
|
"send_reply_inbox__reply_id__send_post",
|
|
25
29
|
"tag_reply_inbox__reply_id__tag_post"
|
|
26
30
|
],
|
|
@@ -50,12 +54,15 @@
|
|
|
50
54
|
{
|
|
51
55
|
"name": "ReplyComposer",
|
|
52
56
|
"file": "components/reply-composer.tsx",
|
|
53
|
-
"purpose": "Write a reply, draft or reshape it with AI (confirm, billable) and send it
|
|
57
|
+
"purpose": "Write a reply, draft or reshape it with AI (confirm, billable) and send it from the thread's own mailbox, number or LinkedIn account to its contact (confirm, external); picks a sender when an SMS or LinkedIn thread names none.",
|
|
54
58
|
"operations": [
|
|
55
59
|
"generate_inbox_draft_inbox_drafts_generate_post",
|
|
60
|
+
"list_inbox_linkedin_accounts_inbox_linkedin_accounts_get",
|
|
61
|
+
"list_sms_phone_numbers_sms_phone_numbers_get",
|
|
56
62
|
"send_reply_inbox__reply_id__send_post"
|
|
57
63
|
],
|
|
58
64
|
"tiers": [
|
|
65
|
+
"read",
|
|
59
66
|
"billable",
|
|
60
67
|
"external"
|
|
61
68
|
]
|
|
@@ -89,6 +96,19 @@
|
|
|
89
96
|
"purpose": "Small status pill shared by the thread view and tag panel.",
|
|
90
97
|
"operations": [],
|
|
91
98
|
"tiers": []
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "RecordConversationsTab",
|
|
102
|
+
"file": "components/record-conversations-tab.tsx",
|
|
103
|
+
"purpose": "The email conversations with one contact (matched by address), with Load more and a calm answer for a thread that 404s; an extraDetailTabs entry for ContactsScreen.",
|
|
104
|
+
"operations": [
|
|
105
|
+
"list_inbox_mailboxes_inbox_mailboxes_get",
|
|
106
|
+
"search_inbox_emails_inbox_emails_search_post",
|
|
107
|
+
"get_reply_inbox__reply_id__get"
|
|
108
|
+
],
|
|
109
|
+
"tiers": [
|
|
110
|
+
"read"
|
|
111
|
+
]
|
|
92
112
|
}
|
|
93
113
|
],
|
|
94
114
|
"hooks": [
|
|
@@ -157,6 +177,7 @@
|
|
|
157
177
|
}
|
|
158
178
|
],
|
|
159
179
|
"scopes": [
|
|
180
|
+
"campaigns:read",
|
|
160
181
|
"inbox:read",
|
|
161
182
|
"inbox:run",
|
|
162
183
|
"inbox:write"
|
|
@@ -186,9 +207,15 @@
|
|
|
186
207
|
"decision": "Filter by assignee",
|
|
187
208
|
"default": "Not offered: the API cannot filter by an arbitrary assignee email (an org key owns no threads). Filter thread.assignees client-side",
|
|
188
209
|
"askHuman": false
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"decision": "Which mailbox, number or LinkedIn account a reply is sent from",
|
|
213
|
+
"default": "The thread's own (mailbox / from_number / linkedin_account_id on the thread); an SMS or LinkedIn thread without one shows a sender picker",
|
|
214
|
+
"askHuman": false
|
|
189
215
|
}
|
|
190
216
|
],
|
|
191
217
|
"peerDependencies": {
|
|
192
|
-
"react": ">=18"
|
|
218
|
+
"react": ">=18",
|
|
219
|
+
"lucide-react": ">=0.400.0"
|
|
193
220
|
}
|
|
194
221
|
}
|
|
@@ -25,8 +25,12 @@ import {
|
|
|
25
25
|
type InboxChannel,
|
|
26
26
|
type InboxThread,
|
|
27
27
|
} from "../lib/inbox-types";
|
|
28
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
29
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
28
30
|
|
|
29
31
|
export interface InboxListProps {
|
|
32
|
+
/** URL param the host screen reads for this record type. With it the name is a real link (Cmd-click opens it in a new tab); without it a plain button. */
|
|
33
|
+
linkParam?: string;
|
|
30
34
|
onSelect?: (thread: InboxThread) => void;
|
|
31
35
|
selectedId?: string | null;
|
|
32
36
|
/** Start with these filters (for example a tag id from the URL). */
|
|
@@ -62,7 +66,7 @@ function LoadingRows() {
|
|
|
62
66
|
);
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
export function InboxList({ onSelect, selectedId, initialFilters, pageSize = 25, onFiltersChange, className }: InboxListProps) {
|
|
69
|
+
export function InboxList({ linkParam, onSelect, selectedId, initialFilters, pageSize = 25, onFiltersChange, className }: InboxListProps) {
|
|
66
70
|
const [filters, setFilters] = useState<InboxFilters>(initialFilters ?? {});
|
|
67
71
|
const [page, setPage] = useState(1);
|
|
68
72
|
const { threads, loading, error, hasNext, total, refetch } = useInboxThreads(filters, page, pageSize);
|
|
@@ -123,9 +127,9 @@ export function InboxList({ onSelect, selectedId, initialFilters, pageSize = 25,
|
|
|
123
127
|
key={`${thread.channel}:${thread.id}`}
|
|
124
128
|
data-state={selectedId === thread.id ? "selected" : undefined}
|
|
125
129
|
className="cursor-pointer"
|
|
126
|
-
|
|
130
|
+
{...rowActivationProps(() => onSelect?.(thread))}
|
|
127
131
|
>
|
|
128
|
-
<TableCell className="max-w-48 truncate font-medium">{contactLabel(thread)}</TableCell>
|
|
132
|
+
<TableCell className="max-w-48 truncate font-medium">{onSelect ? <RecordLink param={linkParam} id={thread.id} onOpen={() => onSelect(thread)}>{contactLabel(thread)}</RecordLink> : contactLabel(thread)}</TableCell>
|
|
129
133
|
<TableCell className="max-w-80 truncate text-muted-foreground">
|
|
130
134
|
{thread.subject ? <span className="text-foreground">{thread.subject} </span> : null}
|
|
131
135
|
{previewText(thread)}
|
|
@@ -1,49 +1,316 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* InboxScreen: the finished
|
|
5
|
-
* reply composer and tag/assign panel.
|
|
4
|
+
* InboxScreen: the finished inbox. A stat strip, a searchable and filterable conversation list on the left, and on the
|
|
5
|
+
* right the open conversation with its reply composer and tag/assign panel. Deep link: `?id=<channel>:<thread id>`.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* (
|
|
7
|
+
* Flows: browse and page the queue; filter by channel, status and tag; search the loaded page; open a conversation;
|
|
8
|
+
* read it (unsent drafts flagged); draft with AI (billable, confirmed) and send (external, confirmed); tag and assign
|
|
9
|
+
* (external, confirmed); clear filters; connect a mailbox (link out to graph8, OAuth only exists there).
|
|
10
|
+
*
|
|
11
|
+
* SDK operations: none of its own. It composes useInboxThreads (GET /inbox, read), ThreadView (read), ReplyComposer
|
|
12
|
+
* (billable draft, external send) and TagAndAssign (external). The confirm dialogs live in those parts.
|
|
13
|
+
*
|
|
14
|
+
* BEHAVIOUR CHANGE (versus the original two-pane InboxScreen): it now writes `?id=` and `?view=` to the host page URL and
|
|
15
|
+
* renders its own ScreenLayout header ("Inbox" h1, stats). Opt out with `urlState={false}` (selection kept in component
|
|
16
|
+
* state, the URL untouched) and/or `hideHeader` (no title or stats, just the toolbar and the two panes). Defaults keep the new look.
|
|
17
|
+
*
|
|
18
|
+
* Props are backwards compatible: `initialFilters`, `availableTags`, `className` as before; everything else is optional glue.
|
|
9
19
|
*/
|
|
10
|
-
import { useMemo, useState } from "react";
|
|
20
|
+
import { useMemo, useState, type ReactNode } from "react";
|
|
21
|
+
import { Inbox as InboxIcon, Mail, RefreshCw } from "lucide-react";
|
|
22
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
23
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
11
24
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
25
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
26
|
+
import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
|
|
27
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
28
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
29
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
30
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
31
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
32
|
+
import { useInboxThread } from "../hooks/use-inbox-thread";
|
|
33
|
+
import { useInboxThreads, type InboxFilters } from "../hooks/use-inbox-threads";
|
|
34
|
+
import {
|
|
35
|
+
INBOX_CHANNELS,
|
|
36
|
+
INBOX_STATUSES,
|
|
37
|
+
contactLabel,
|
|
38
|
+
formatWhen,
|
|
39
|
+
latestMessage,
|
|
40
|
+
previewText,
|
|
41
|
+
type InboxChannel,
|
|
42
|
+
type InboxTag,
|
|
43
|
+
type InboxThread,
|
|
44
|
+
} from "../lib/inbox-types";
|
|
15
45
|
import { ReplyComposer } from "./reply-composer";
|
|
16
46
|
import { TagAndAssign } from "./tag-and-assign";
|
|
17
47
|
import { ThreadView } from "./thread-view";
|
|
48
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
18
49
|
|
|
19
50
|
export interface InboxScreenProps {
|
|
20
51
|
initialFilters?: InboxFilters;
|
|
21
52
|
/** Tags the person can apply. There is no list-tags operation; keep this list in your app. */
|
|
22
53
|
availableTags?: InboxTag[];
|
|
23
54
|
className?: string;
|
|
55
|
+
/** Extra buttons next to Refresh. */
|
|
56
|
+
toolbarActions?: ReactNode;
|
|
57
|
+
/** Rendered at the end of each conversation row. */
|
|
58
|
+
renderRowActions?: (thread: InboxThread) => ReactNode;
|
|
59
|
+
/** Rendered in the header of the open conversation. */
|
|
60
|
+
renderDetailActions?: (thread: InboxThread) => ReactNode;
|
|
61
|
+
/** Extra tabs next to "Conversation" (for example the contact's CRM record). */
|
|
62
|
+
extraDetailTabs?: Array<{ id: string; label: string; render: (thread: InboxThread) => ReactNode }>;
|
|
63
|
+
/** Page size of the list. Default 25. */
|
|
64
|
+
pageSize?: number;
|
|
65
|
+
/** Keep the open conversation and full-page view in the page URL (`?id=`, `?view=`). Default true; false keeps them in component state and leaves the URL alone. */
|
|
66
|
+
urlState?: boolean;
|
|
67
|
+
/** Skip the screen header (title, description, stats) when the host page already has one. Default false. */
|
|
68
|
+
hideHeader?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** `useUrlState` or plain state, chosen once per mount (the flag must not change while mounted). */
|
|
72
|
+
function useSelectionState(key: string, urlState: boolean): [string | undefined, (v: string | null) => void] {
|
|
73
|
+
const [fromUrl, setFromUrl] = useUrlState(key);
|
|
74
|
+
const [local, setLocal] = useState<string | null>(null);
|
|
75
|
+
return urlState ? [fromUrl, setFromUrl] : [local ?? undefined, setLocal];
|
|
24
76
|
}
|
|
25
77
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
78
|
+
type Selected = { channel: string; id: string };
|
|
79
|
+
const keyOf = (t: Selected) => `${t.channel}:${t.id}`;
|
|
80
|
+
function parseKey(value: string): Selected | null {
|
|
81
|
+
const at = value.indexOf(":");
|
|
82
|
+
return at > 0 && at < value.length - 1 ? { channel: value.slice(0, at), id: value.slice(at + 1) } : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function statusTone(status?: string | null) {
|
|
86
|
+
const s = (status ?? "").toUpperCase();
|
|
87
|
+
return s === "DRAFT" ? "warning" : s === "SENT" ? "positive" : s === "AI_RESPONDED" ? "info" : "neutral";
|
|
88
|
+
}
|
|
29
89
|
|
|
90
|
+
function ThreadRow({ thread, selected, onOpen, actions }: { thread: InboxThread; selected: boolean; onOpen: () => void; actions?: ReactNode }) {
|
|
91
|
+
const hasDraft = thread.messages.some((m) => m.is_draft);
|
|
30
92
|
return (
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<
|
|
36
|
-
{(thread
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
93
|
+
<li className={`flex items-start gap-1 border-b border-border ${selected ? "bg-muted" : "hover:bg-muted/50"}`}>
|
|
94
|
+
<button type="button" aria-current={selected ? "true" : undefined} onClick={onOpen} className="flex min-w-0 flex-1 items-start gap-3 p-3 text-left">
|
|
95
|
+
<EntityAvatar name={contactLabel(thread)} kind="person" size="sm" />
|
|
96
|
+
<span className="min-w-0 flex-1">
|
|
97
|
+
<span className="flex items-center justify-between gap-2">
|
|
98
|
+
<span className="truncate text-sm font-medium">{contactLabel(thread)}</span>
|
|
99
|
+
<span className="shrink-0 text-xs text-muted-foreground">{formatWhen(thread.updated_at ?? latestMessage(thread)?.date)}</span>
|
|
100
|
+
</span>
|
|
101
|
+
<span className="block truncate text-sm">{thread.subject ?? ""}</span>
|
|
102
|
+
<span className="block truncate text-xs text-muted-foreground">{previewText(thread)}</span>
|
|
103
|
+
<span className="mt-1 flex flex-wrap items-center gap-1">
|
|
104
|
+
<StatusPill tone="neutral">{String(thread.channel)}</StatusPill>
|
|
105
|
+
{thread.status ? <StatusPill tone={statusTone(thread.status)}>{thread.status.toLowerCase().replace("_", " ")}</StatusPill> : null}
|
|
106
|
+
{hasDraft ? <StatusPill tone="warning" dot>Draft waiting</StatusPill> : null}
|
|
107
|
+
{thread.tags.map((t) => <StatusPill key={t.id ?? t.name} tone="info">{t.name ?? t.id}</StatusPill>)}
|
|
108
|
+
</span>
|
|
109
|
+
</span>
|
|
110
|
+
</button>
|
|
111
|
+
{actions ? <div className="shrink-0 p-2">{actions}</div> : null}
|
|
112
|
+
</li>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function ChannelFilter({ value, onChange }: { value?: InboxChannel; onChange: (next?: InboxChannel) => void }) {
|
|
117
|
+
return (
|
|
118
|
+
<div role="group" aria-label="Channel" className="flex gap-1">
|
|
119
|
+
<Button size="sm" variant={value ? "outline" : "default"} onClick={() => onChange(undefined)}>All</Button>
|
|
120
|
+
{INBOX_CHANNELS.map((c) => (
|
|
121
|
+
<Button key={c.value} size="sm" variant={value === c.value ? "default" : "outline"} onClick={() => onChange(c.value)}>{c.label}</Button>
|
|
122
|
+
))}
|
|
47
123
|
</div>
|
|
48
124
|
);
|
|
49
125
|
}
|
|
126
|
+
|
|
127
|
+
function Filters({ filters, tags, update }: { filters: InboxFilters; tags: InboxTag[]; update: (patch: Partial<InboxFilters>) => void }) {
|
|
128
|
+
const selectClass = "h-8 rounded-md border border-border bg-background px-2 text-sm";
|
|
129
|
+
return (
|
|
130
|
+
<>
|
|
131
|
+
<ChannelFilter value={filters.channel} onChange={(channel) => update({ channel })} />
|
|
132
|
+
<select aria-label="Status" className={selectClass} value={filters.status ?? ""} onChange={(e) => update({ status: e.target.value || undefined })}>
|
|
133
|
+
<option value="">Any status</option>
|
|
134
|
+
{INBOX_STATUSES.map((s) => <option key={s} value={s}>{s.replace("_", " ").toLowerCase()}</option>)}
|
|
135
|
+
</select>
|
|
136
|
+
{tags.length > 0 ? (
|
|
137
|
+
<select aria-label="Tag" className={selectClass} value={filters.tag ?? ""} onChange={(e) => update({ tag: e.target.value || undefined })}>
|
|
138
|
+
<option value="">Any tag</option>
|
|
139
|
+
{tags.map((t) => <option key={t.id} value={t.id ?? ""}>{t.name ?? t.id}</option>)}
|
|
140
|
+
</select>
|
|
141
|
+
) : null}
|
|
142
|
+
</>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function ListEmpty({ filtered, onClear }: { filtered: boolean; onClear: () => void }) {
|
|
147
|
+
const brand = useBrand();
|
|
148
|
+
if (filtered) {
|
|
149
|
+
return <EmptyState title="No conversations match" description="Nothing fits these filters or this search." action={<Button size="sm" variant="outline" onClick={onClear}>Clear filters</Button>} />;
|
|
150
|
+
}
|
|
151
|
+
return (
|
|
152
|
+
<EmptyState
|
|
153
|
+
icon={<Mail className="size-6" />}
|
|
154
|
+
title="No conversations yet"
|
|
155
|
+
description={`Replies from sequences, SMS and LinkedIn land here. Connect a mailbox in ${brand.linkLabel} (Google and Microsoft sign-in only exist there), then enroll contacts in a sequence.`}
|
|
156
|
+
action={<a className="text-sm underline" href={brand.appUrl} target="_blank" rel="noreferrer">Connect in {brand.linkLabel}</a>}
|
|
157
|
+
/>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function ThreadDetails({ thread }: { thread: Selected }) {
|
|
162
|
+
const { data: t, loading, error, refetch } = useInboxThread(thread.id, thread.channel);
|
|
163
|
+
if (error) return <ApiError error={error} title="Could not load this conversation" onRetry={() => void refetch()} />;
|
|
164
|
+
if (!t) return loading ? <Skeleton className="h-24 w-full" /> : null;
|
|
165
|
+
const dash = "";
|
|
166
|
+
return (
|
|
167
|
+
<KeyValueList
|
|
168
|
+
columns={1}
|
|
169
|
+
items={[
|
|
170
|
+
{ label: "Contact", value: t.contact?.name || dash },
|
|
171
|
+
{ label: "Email", value: t.contact?.email || dash },
|
|
172
|
+
{ label: "Phone", value: t.contact?.phone || dash },
|
|
173
|
+
{ label: "Company", value: t.contact?.company || dash },
|
|
174
|
+
{ label: "Channel", value: String(t.channel) },
|
|
175
|
+
{ label: "Status", value: t.status ? t.status.toLowerCase().replace("_", " ") : dash },
|
|
176
|
+
{ label: "Tags", value: t.tags.length ? t.tags.map((x) => x.name ?? x.id).join(", ") : dash },
|
|
177
|
+
{ label: "Assigned to", value: t.assignees.length ? t.assignees.map((a) => a.name || a.email).join(", ") : dash },
|
|
178
|
+
{ label: "Messages", value: `${t.messages.length}${t.messages.some((m) => m.is_draft) ? " (includes an unsent draft)" : ""}` },
|
|
179
|
+
{ label: "Started", value: formatWhen(t.created_at) || dash },
|
|
180
|
+
{ label: "Last activity", value: formatWhen(t.updated_at) || dash },
|
|
181
|
+
]}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function ExtraTab({ thread, render }: { thread: Selected; render: (thread: InboxThread) => ReactNode }) {
|
|
187
|
+
const { data, loading, error, refetch } = useInboxThread(thread.id, thread.channel);
|
|
188
|
+
if (error) return <ApiError error={error} title="Could not load this conversation" onRetry={() => void refetch()} />;
|
|
189
|
+
if (!data) return loading ? <Skeleton className="h-24 w-full" /> : null;
|
|
190
|
+
return <>{render(data)}</>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function InboxScreen({ initialFilters, availableTags, className, toolbarActions, renderRowActions, renderDetailActions, extraDetailTabs, pageSize = 25, urlState = true, hideHeader = false }: InboxScreenProps) {
|
|
194
|
+
const [filters, setFilters] = useState<InboxFilters>(initialFilters ?? {});
|
|
195
|
+
const [page, setPage] = useState(1);
|
|
196
|
+
const [search, setSearch] = useState("");
|
|
197
|
+
const [openKey, setOpenKey] = useSelectionState("id", urlState);
|
|
198
|
+
const [view, setView] = useSelectionState("view", urlState);
|
|
199
|
+
const { threads, loading, error, hasNext, total, refetch } = useInboxThreads(filters, page, pageSize);
|
|
200
|
+
const open = openKey ? parseKey(openKey) : null;
|
|
201
|
+
const expanded = view === "full" && open != null;
|
|
202
|
+
|
|
203
|
+
const tags = useMemo(() => {
|
|
204
|
+
const seen = new Map<string, InboxTag>();
|
|
205
|
+
for (const t of [...(availableTags ?? []), ...threads.flatMap((th) => th.tags)]) if (t.id) seen.set(t.id, t);
|
|
206
|
+
return [...seen.values()];
|
|
207
|
+
}, [availableTags, threads]);
|
|
208
|
+
const needle = search.trim().toLowerCase();
|
|
209
|
+
const rows = useMemo(
|
|
210
|
+
() => (needle ? threads.filter((t) => `${contactLabel(t)} ${t.contact?.email ?? ""} ${t.contact?.company ?? ""} ${t.subject ?? ""} ${previewText(t, 300)}`.toLowerCase().includes(needle)) : threads),
|
|
211
|
+
[threads, needle],
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const update = (patch: Partial<InboxFilters>) => {
|
|
215
|
+
setFilters((f) => ({ ...f, ...patch }));
|
|
216
|
+
setPage(1);
|
|
217
|
+
setOpenKey(null);
|
|
218
|
+
};
|
|
219
|
+
const clear = () => { setSearch(""); setFilters({}); setPage(1); setOpenKey(null); };
|
|
220
|
+
const filtered = Boolean(needle || filters.channel || filters.status || filters.tag || filters.sequenceId);
|
|
221
|
+
const drafts = threads.filter((t) => t.messages.some((m) => m.is_draft)).length;
|
|
222
|
+
const count = (c: string) => threads.filter((t) => t.channel === c).length;
|
|
223
|
+
const first = open ? threads.find((t) => t.id === open.id && t.channel === open.channel) : undefined;
|
|
224
|
+
|
|
225
|
+
const panes = (
|
|
226
|
+
<>
|
|
227
|
+
<div className={expanded ? "grid gap-4" : "grid gap-4 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]"}>
|
|
228
|
+
<div className={expanded ? "hidden" : "min-w-0 space-y-2"}>
|
|
229
|
+
{error ? (
|
|
230
|
+
<ApiError error={error} title="Could not load the inbox" onRetry={() => void refetch()} />
|
|
231
|
+
) : loading && threads.length === 0 ? (
|
|
232
|
+
<div aria-busy="true" className="space-y-2">{Array.from({ length: 5 }, (_, i) => <Skeleton key={i} className="h-16 w-full" />)}</div>
|
|
233
|
+
) : rows.length === 0 ? (
|
|
234
|
+
<ListEmpty filtered={filtered} onClear={clear} />
|
|
235
|
+
) : (
|
|
236
|
+
<ul aria-label="Conversations" className="rounded-md border border-border">
|
|
237
|
+
{rows.map((t) => (
|
|
238
|
+
<ThreadRow key={keyOf(t)} thread={t} selected={open != null && keyOf(open) === keyOf(t)} onOpen={() => setOpenKey(keyOf(t))} actions={renderRowActions?.(t)} />
|
|
239
|
+
))}
|
|
240
|
+
</ul>
|
|
241
|
+
)}
|
|
242
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
243
|
+
<span>{total != null ? `${total.toLocaleString()} conversations` : `Page ${page}`}</span>
|
|
244
|
+
<div className="flex gap-2">
|
|
245
|
+
<Button size="sm" variant="outline" disabled={page <= 1 || loading} onClick={() => { setPage((p) => p - 1); setOpenKey(null); }}>Previous</Button>
|
|
246
|
+
<Button size="sm" variant="outline" disabled={!hasNext || loading} onClick={() => { setPage((p) => p + 1); setOpenKey(null); }}>Next</Button>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
<div className="min-w-0">
|
|
251
|
+
{open ? (
|
|
252
|
+
<DetailPanel
|
|
253
|
+
key={keyOf(open)}
|
|
254
|
+
title={first ? first.subject || contactLabel(first) : "Conversation"}
|
|
255
|
+
subtitle={first ? `${contactLabel(first)}${first.contact?.company ? ` at ${first.contact.company}` : ""}` : undefined}
|
|
256
|
+
avatar={first ? <EntityAvatar name={contactLabel(first)} kind="person" /> : undefined}
|
|
257
|
+
onClose={() => { setOpenKey(null); setView(null); }}
|
|
258
|
+
onExpand={expanded ? undefined : () => setView("full")}
|
|
259
|
+
onCollapse={expanded ? () => setView(null) : undefined}
|
|
260
|
+
tabs={[
|
|
261
|
+
{
|
|
262
|
+
id: "conversation",
|
|
263
|
+
label: "Conversation",
|
|
264
|
+
content: (
|
|
265
|
+
<ThreadView thread={first ?? open}>
|
|
266
|
+
{(thread, reload) => (
|
|
267
|
+
<div className="space-y-3">
|
|
268
|
+
{renderDetailActions ? <div className="flex flex-wrap gap-2">{renderDetailActions(thread)}</div> : null}
|
|
269
|
+
<ReplyComposer thread={thread} onSent={reload} />
|
|
270
|
+
<TagAndAssign thread={thread} availableTags={tags} onChanged={reload} />
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
</ThreadView>
|
|
274
|
+
),
|
|
275
|
+
},
|
|
276
|
+
{ id: "details", label: "Details", content: <ThreadDetails thread={open} /> },
|
|
277
|
+
...(extraDetailTabs ?? []).map((tab) => ({ id: tab.id, label: tab.label, content: <ExtraTab thread={open} render={tab.render} /> })),
|
|
278
|
+
]}
|
|
279
|
+
/>
|
|
280
|
+
) : (
|
|
281
|
+
<EmptyState title="Select a conversation" description="Pick a reply on the left to read it and respond." />
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</>
|
|
286
|
+
);
|
|
287
|
+
if (hideHeader) {
|
|
288
|
+
return (
|
|
289
|
+
<div className={className ? `space-y-4 ${className}` : "space-y-4"}>
|
|
290
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
291
|
+
<Toolbar search={{ value: search, onChange: setSearch, placeholder: "Search this page" }} filters={<Filters filters={filters} tags={tags} update={update} />} />
|
|
292
|
+
<div className="flex gap-2"><Button size="sm" variant="outline" onClick={() => void refetch()}><RefreshCw className="mr-1 size-4" />Refresh</Button>{toolbarActions}</div>
|
|
293
|
+
</div>
|
|
294
|
+
{panes}
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return (
|
|
299
|
+
<ScreenLayout
|
|
300
|
+
className={className}
|
|
301
|
+
title="Inbox"
|
|
302
|
+
description="Every reply across email, SMS and LinkedIn in one queue."
|
|
303
|
+
icon={<InboxIcon className="size-5" />}
|
|
304
|
+
actions={<><Button size="sm" variant="outline" onClick={() => void refetch()}><RefreshCw className="mr-1 size-4" />Refresh</Button>{toolbarActions}</>}
|
|
305
|
+
stats={[
|
|
306
|
+
{ label: "Conversations", value: total != null ? total.toLocaleString() : threads.length, hint: total != null ? "in this view" : "on this page" },
|
|
307
|
+
{ label: "Drafts waiting", value: drafts, hint: "on this page", tone: drafts > 0 ? "warning" : "neutral" },
|
|
308
|
+
{ label: "Email threads", value: count("email"), hint: "on this page" },
|
|
309
|
+
{ label: "Text and LinkedIn", value: count("sms") + count("linkedin"), hint: "on this page" },
|
|
310
|
+
]}
|
|
311
|
+
toolbar={<Toolbar search={{ value: search, onChange: setSearch, placeholder: "Search this page" }} filters={<Filters filters={filters} tags={tags} update={update} />} />}
|
|
312
|
+
>
|
|
313
|
+
{panes}
|
|
314
|
+
</ScreenLayout>
|
|
315
|
+
);
|
|
316
|
+
}
|