@graph8/sdk 0.370.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 +19 -0
- package/blocks/_foundation/README.md +14 -0
- package/blocks/_foundation/block.json +44 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- 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 +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -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 +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- 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 +53 -11
- 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/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -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 +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- 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/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +6 -0
- package/blocks/crm/block.json +53 -4
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contacts-screen.tsx +31 -13
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +151 -2
- 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 +2 -2
- 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 +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- 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 +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +4 -1
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/enrichment/README.md +16 -6
- package/blocks/enrichment/block.json +19 -7
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +107 -0
- 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 +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- 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 +51 -17
- 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 +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +5 -0
- package/blocks/inbox/block.json +15 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -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 +20 -1
- 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 +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- 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 +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- 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 +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- 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/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- 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 +36 -13
- 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/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +113 -1
- 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-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 +48 -1
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- 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 +3 -2
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +6 -0
- package/blocks/sequencer/block.json +28 -1
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +19 -20
- package/blocks/sequencer/components/sequence-detail.tsx +4 -2
- package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequences-screen.tsx +3 -0
- package/blocks/sequencer/components/step-editor.tsx +11 -11
- package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- 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 +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- 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 +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -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-screen.tsx +33 -6
- 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/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +1 -1
- package/blocks/workbench/components/workbench-screen.tsx +15 -5
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +536 -392
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +7 -3
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/package.json +1 -1
- 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 +17 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +29 -95
- 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 +36 -8
- 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 +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +12 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- 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 +23 -6
- 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 +9 -4
- package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- 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/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +21 -5
- 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 +8 -4
- 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 +20 -5
- 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 +27 -66
- 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 +25 -7
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +4 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +10 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +7 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +16 -3
- package/styles/styles.css +1 -1
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
|
@@ -23,6 +23,7 @@ import { SplitView } from "@/g8/components/screen/split-view";
|
|
|
23
23
|
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
24
24
|
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
25
25
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
26
|
+
import { useCurrentMember } from "@/g8/pickers/hooks/use-current-member";
|
|
26
27
|
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
27
28
|
import { g8Get } from "@/g8/lib/g8-client";
|
|
28
29
|
import { dataOf, formatDuration, type CallSummary } from "../lib/dialer-types";
|
|
@@ -76,7 +77,9 @@ export function CallHistory({ contactId, userEmail, sessionId, limit = 25, idPar
|
|
|
76
77
|
const [outcome, setOutcome] = useState("");
|
|
77
78
|
const [openId, setOpenId] = useUrlState(idParam);
|
|
78
79
|
const [view, setView] = useUrlState("view");
|
|
79
|
-
|
|
80
|
+
// Unscoped history defaults to YOUR calls (the signed-in member); typing another rep's email still overrides.
|
|
81
|
+
const me = useCurrentMember({ enabled: !userEmail && !contactId && !sessionId });
|
|
82
|
+
const rep = useDebounced(userEmail || typedEmail.trim() || me.email || "");
|
|
80
83
|
const scoped = Boolean(contactId || rep || sessionId);
|
|
81
84
|
const q = useG8Query(
|
|
82
85
|
async (signal) => {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Org-wide history across all reps: use POST /voice/call-results/search (voice.searchCallResults) from a server recipe.
|
|
10
10
|
*/
|
|
11
11
|
import { useState } from "react";
|
|
12
|
-
import { Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
12
|
+
import { Button, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
13
13
|
import { ApiError } from "@/g8/components/api-error";
|
|
14
14
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
15
15
|
import { g8Get } from "@/g8/lib/g8-client";
|
|
@@ -39,12 +39,16 @@ export function CallLog({ linkParam, contactId, userEmail, sessionId, limit = 25
|
|
|
39
39
|
const [typedEmail, setTypedEmail] = useState("");
|
|
40
40
|
const repEmail = userEmail || typedEmail.trim();
|
|
41
41
|
const scoped = Boolean(contactId || repEmail || sessionId);
|
|
42
|
+
// The calls API pages by a growing `limit`: "Load more" asks for another page-worth and keeps the earlier calls.
|
|
43
|
+
const scope = `${contactId}|${repEmail}|${sessionId}|${limit}`;
|
|
44
|
+
const [size, setSize] = useState({ scope, rows: limit });
|
|
45
|
+
const rowsWanted = size.scope === scope ? size.rows : limit;
|
|
42
46
|
const q = useG8Query(
|
|
43
47
|
async (signal) => {
|
|
44
|
-
const env = await g8Get("/voice/dialer/calls", { contact_id: contactId, user_email: repEmail || undefined, parallel_session_id: sessionId, limit }, { signal });
|
|
48
|
+
const env = await g8Get("/voice/dialer/calls", { contact_id: contactId, user_email: repEmail || undefined, parallel_session_id: sessionId, limit: rowsWanted }, { signal });
|
|
45
49
|
return dataOf<{ calls?: CallSummary[] }>(env).calls ?? [];
|
|
46
50
|
},
|
|
47
|
-
[contactId, repEmail, sessionId,
|
|
51
|
+
[contactId, repEmail, sessionId, rowsWanted],
|
|
48
52
|
{ enabled: scoped },
|
|
49
53
|
);
|
|
50
54
|
|
|
@@ -62,7 +66,8 @@ export function CallLog({ linkParam, contactId, userEmail, sessionId, limit = 25
|
|
|
62
66
|
if (calls.length === 0) return <EmptyState className={className} title="No calls yet" description="Calls appear here after the dialer places them." />;
|
|
63
67
|
|
|
64
68
|
return (
|
|
65
|
-
<
|
|
69
|
+
<div className={className}>
|
|
70
|
+
<Table aria-label="Call log">
|
|
66
71
|
<TableHeader>
|
|
67
72
|
<TableRow><TableHead>When</TableHead><TableHead>Number</TableHead><TableHead>Outcome</TableHead><TableHead>Length</TableHead><TableHead>Summary</TableHead></TableRow>
|
|
68
73
|
</TableHeader>
|
|
@@ -78,5 +83,12 @@ export function CallLog({ linkParam, contactId, userEmail, sessionId, limit = 25
|
|
|
78
83
|
))}
|
|
79
84
|
</TableBody>
|
|
80
85
|
</Table>
|
|
86
|
+
{calls.length >= rowsWanted ? (
|
|
87
|
+
<div className="flex items-center justify-between gap-3 py-2 text-sm text-muted-foreground">
|
|
88
|
+
<span role="status">Showing {calls.length.toLocaleString()} calls</span>
|
|
89
|
+
<Button size="sm" variant="outline" loading={q.loading} onClick={() => setSize({ scope, rows: rowsWanted + limit })}>Load more</Button>
|
|
90
|
+
</div>
|
|
91
|
+
) : null}
|
|
92
|
+
</div>
|
|
81
93
|
);
|
|
82
94
|
}
|
|
@@ -50,7 +50,7 @@ function Stat({ label, value }: { label: string; value: string | number }) {
|
|
|
50
50
|
return (
|
|
51
51
|
<div className="rounded-lg border p-3">
|
|
52
52
|
<div className="text-xs uppercase text-muted-foreground">{label}</div>
|
|
53
|
-
<div className="text-lg font-
|
|
53
|
+
<div className="text-lg font-medium capitalize">{value}</div>
|
|
54
54
|
</div>
|
|
55
55
|
);
|
|
56
56
|
}
|
|
@@ -71,7 +71,7 @@ export function WaitingContacts({ sessionId, version, onSkipped }: { sessionId:
|
|
|
71
71
|
const rows = (grid.data ?? []).filter((r) => !r.callresult?.disposition);
|
|
72
72
|
return (
|
|
73
73
|
<div className="space-y-2">
|
|
74
|
-
<h3 className="text-sm font-
|
|
74
|
+
<h3 className="text-sm font-medium">Up next</h3>
|
|
75
75
|
{skip.error ? <ApiError error={skip.error} title="Could not skip that contact" /> : null}
|
|
76
76
|
<Table aria-label="Contacts waiting">
|
|
77
77
|
<TableHeader>
|
|
@@ -141,7 +141,7 @@ export function CallQueue({ sessionId: initialId, userId, userEmail, listId, lis
|
|
|
141
141
|
return (
|
|
142
142
|
<div className={`space-y-4 ${className ?? ""}`}>
|
|
143
143
|
<header className="flex flex-wrap items-center justify-between gap-2">
|
|
144
|
-
<h2 className="text-base font-
|
|
144
|
+
<h2 className="text-base font-medium">{session.detail?.name ?? "Dialing session"}</h2>
|
|
145
145
|
<div className="flex gap-2">
|
|
146
146
|
<Button disabled={!stats || over || nextBatch.loading} onClick={() => setConfirming(true)}>Dial next batch</Button>
|
|
147
147
|
<Button variant="outline" disabled={!stats || over} onClick={() => void pause.mutate(undefined)}>Pause</Button>
|
|
@@ -22,11 +22,17 @@ import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader,
|
|
|
22
22
|
import { ApiError } from "@/g8/components/api-error";
|
|
23
23
|
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
24
24
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
|
+
import { useCurrentMember } from "@/g8/pickers/hooks/use-current-member";
|
|
26
|
+
import { MemberPicker } from "@/g8/pickers/components/member-picker";
|
|
27
|
+
import type { MemberOption } from "@/g8/pickers/components/member-picker";
|
|
25
28
|
import { g8Get } from "@/g8/lib/g8-client";
|
|
26
29
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
27
30
|
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
28
31
|
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
29
32
|
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
33
|
+
import { useUrlFilters } from "@/g8/hooks/use-url-filters";
|
|
34
|
+
import { ListSelect } from "@/g8/crm/components/list-select";
|
|
35
|
+
import { MoreFilters, RangeFilter, SelectFilter, TextFilter } from "@/g8/components/screen/filter-controls";
|
|
30
36
|
import { ListPicker, type ListSummary } from "@/g8/lists/components/list-picker";
|
|
31
37
|
import { SplitView } from "@/g8/components/screen/split-view";
|
|
32
38
|
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
@@ -43,7 +49,10 @@ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
|
43
49
|
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
44
50
|
|
|
45
51
|
export interface DialerScreenProps {
|
|
46
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* The rep these calls belong to. Optional: when omitted the rep is the signed-in team member (GET /team-members/me,
|
|
54
|
+
* a personal API key) and a MemberPicker switches rep. A passed `userId` always wins and hides the switcher.
|
|
55
|
+
*/
|
|
47
56
|
userId?: string;
|
|
48
57
|
userEmail?: string;
|
|
49
58
|
/** Skip the list picker. */
|
|
@@ -79,15 +88,6 @@ interface SessionRow {
|
|
|
79
88
|
|
|
80
89
|
const STATUSES = ["ACTIVE", "PAUSED", "COMPLETED"];
|
|
81
90
|
|
|
82
|
-
function useDebounced<T>(value: T, ms = 300): T {
|
|
83
|
-
const [v, setV] = useState(value);
|
|
84
|
-
useEffect(() => {
|
|
85
|
-
const t = setTimeout(() => setV(value), ms);
|
|
86
|
-
return () => clearTimeout(t);
|
|
87
|
-
}, [value, ms]);
|
|
88
|
-
return v;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
91
|
function sessionTone(status?: string | null) {
|
|
92
92
|
const s = normalizeStatus(status);
|
|
93
93
|
return s === "active" ? "positive" : s === "paused" ? "warning" : "neutral";
|
|
@@ -99,33 +99,56 @@ function created(v: unknown): string {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/** All dialing sessions (GET /voice/dialer/sessions). A row opens the full session record: ?tab=sessions&open=<id>[&view=full]. */
|
|
102
|
+
/** Every filter the sessions API takes (user_email comes from the screen's `userEmail`). */
|
|
103
|
+
const SESSION_FILTERS = ["name", "status", "campaign_id", "list_id", "list_name", "date_from", "date_to", "sort_by", "sort_order"] as const;
|
|
104
|
+
const SESSION_MORE = ["campaign_id", "list_id", "list_name", "date_from", "date_to", "sort_by", "sort_order"] as const;
|
|
105
|
+
|
|
102
106
|
function SessionsTab({ userEmail, onWork }: { userEmail?: string; onWork: (id: string) => void }) {
|
|
103
107
|
// Its own param: `session` belongs to the Session tab (the running session); closing a detail here must never clear it.
|
|
104
108
|
const [openId, setOpenId] = useUrlState("open");
|
|
105
109
|
const [view, setView] = useUrlState("view");
|
|
106
110
|
const [page, setPage] = useState(1);
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const
|
|
111
|
+
// Every filter the sessions API takes lives in the URL (`s_campaign_id=...`): status, name, campaign, list, dates, sort.
|
|
112
|
+
const uf = useUrlFilters(SESSION_FILTERS, { prefix: "s_" });
|
|
113
|
+
const { status, name } = uf.draft;
|
|
114
|
+
const f = uf.filters;
|
|
115
|
+
const search = f.name.trim();
|
|
110
116
|
const q = useG8Query(
|
|
111
117
|
async (signal) => {
|
|
112
|
-
const env = await g8Get("/voice/dialer/sessions", {
|
|
118
|
+
const env = await g8Get("/voice/dialer/sessions", {
|
|
119
|
+
page, page_size: 25, status: f.status || undefined, name: search || undefined, user_email: userEmail,
|
|
120
|
+
campaign_id: f.campaign_id || undefined, list_id: f.list_id || undefined, list_name: f.list_name || undefined,
|
|
121
|
+
date_from: f.date_from ? `${f.date_from}T00:00:00Z` : undefined, date_to: f.date_to ? `${f.date_to}T23:59:59Z` : undefined,
|
|
122
|
+
sort_by: f.sort_by || undefined, sort_order: f.sort_by ? f.sort_order || undefined : undefined,
|
|
123
|
+
}, { signal });
|
|
113
124
|
const d = dataOf<{ sessions?: SessionRow[]; pagination?: { total_items?: number; total_pages?: number } }>(env);
|
|
114
125
|
return { rows: d.sessions ?? [], total: d.pagination?.total_items, pages: d.pagination?.total_pages };
|
|
115
126
|
},
|
|
116
|
-
[page,
|
|
127
|
+
[page, f, search, userEmail],
|
|
128
|
+
{ enabled: uf.ready },
|
|
117
129
|
);
|
|
118
130
|
const rows = q.data?.rows ?? [];
|
|
119
|
-
|
|
131
|
+
useEffect(() => { setPage(1); }, [f]);
|
|
120
132
|
const list = (
|
|
121
133
|
<div className="space-y-3">
|
|
122
134
|
<Toolbar
|
|
123
|
-
search={{ value: name, onChange: (v) =>
|
|
135
|
+
search={{ value: name, onChange: (v) => uf.set("name", v), debounceMs: 0, placeholder: "Search by session name" }}
|
|
124
136
|
filters={
|
|
125
|
-
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
137
|
+
<>
|
|
138
|
+
<select aria-label="Session status" className="h-8 rounded-md border border-border bg-background px-2 text-sm" value={status} onChange={(e) => uf.set("status", e.target.value)}>
|
|
139
|
+
<option value="">Any status</option>
|
|
140
|
+
{STATUSES.map((s) => <option key={s} value={s}>{s.toLowerCase()}</option>)}
|
|
141
|
+
</select>
|
|
142
|
+
<MoreFilters activeCount={SESSION_MORE.filter((k) => uf.draft[k]).length}>
|
|
143
|
+
<TextFilter label="campaign" placeholder="Campaign ID" value={uf.draft.campaign_id} onChange={(v) => uf.set("campaign_id", v)} className="w-40" />
|
|
144
|
+
<ListSelect label="Filter by list" emptyOption="Any list" value={uf.draft.list_id} valueLabel={`List ${uf.draft.list_id}`} pageSize={100} onChange={(l) => uf.set("list_id", l ? String(l.id) : "")} />
|
|
145
|
+
<TextFilter label="list name" placeholder="List name" value={uf.draft.list_name} onChange={(v) => uf.set("list_name", v)} className="w-40" />
|
|
146
|
+
<RangeFilter label="Created" type="date" min={uf.draft.date_from} max={uf.draft.date_to} onMinChange={(v) => uf.set("date_from", v)} onMaxChange={(v) => uf.set("date_to", v)} />
|
|
147
|
+
<SelectFilter label="sort" any="Newest first" value={uf.draft.sort_by} onChange={(v) => uf.set("sort_by", v)} options={[{ value: "created_at", label: "Created" }, { value: "updated_at", label: "Last updated" }, { value: "total_calls", label: "Calls" }]} />
|
|
148
|
+
<SelectFilter label="sort order" any="Descending" value={uf.draft.sort_order} onChange={(v) => uf.set("sort_order", v)} options={[{ value: "asc", label: "Ascending" }]} />
|
|
149
|
+
</MoreFilters>
|
|
150
|
+
{uf.active ? <Button variant="ghost" size="sm" onClick={uf.clear}>Clear filters</Button> : null}
|
|
151
|
+
</>
|
|
129
152
|
}
|
|
130
153
|
/>
|
|
131
154
|
{q.error ? (
|
|
@@ -133,7 +156,7 @@ function SessionsTab({ userEmail, onWork }: { userEmail?: string; onWork: (id: s
|
|
|
133
156
|
) : q.loading && !q.data ? (
|
|
134
157
|
<Skeleton className="h-32 w-full" aria-label="Loading sessions" />
|
|
135
158
|
) : rows.length === 0 ? (
|
|
136
|
-
<EmptyState icon={<Phone className="size-6" />} title={
|
|
159
|
+
<EmptyState icon={<Phone className="size-6" />} title={uf.active ? "No sessions match" : "No sessions yet"} description={uf.active ? "Try a different search or fewer filters." : "Start one in the Session tab. Each session keeps its queue, calls and outcomes."} />
|
|
137
160
|
) : (
|
|
138
161
|
<Table aria-label="Dialing sessions">
|
|
139
162
|
<TableHeader><TableRow><TableHead>Session</TableHead><TableHead>Status</TableHead><TableHead>Calls</TableHead><TableHead>Rep</TableHead><TableHead>Started</TableHead></TableRow></TableHeader>
|
|
@@ -256,11 +279,11 @@ function SessionTab(props: DialerScreenProps & { onCalls: (calls: LiveCall[]) =>
|
|
|
256
279
|
/>
|
|
257
280
|
<div className="space-y-4">
|
|
258
281
|
{props.userId && transport ? (
|
|
259
|
-
<Softphone userId={props.userId} transport={transport} conferenceName={batch?.conference_name} joinMuted={(batch?.batch?.length ?? 1) > 1} />
|
|
282
|
+
<Softphone key={props.userId} userId={props.userId} transport={transport} conferenceName={batch?.conference_name} joinMuted={(batch?.batch?.length ?? 1) > 1} />
|
|
260
283
|
) : props.userId ? (
|
|
261
284
|
<Skeleton className="h-24 w-full" aria-label="Loading softphone" />
|
|
262
285
|
) : (
|
|
263
|
-
<SectionCard title="Softphone"><p className="text-sm text-muted-foreground">
|
|
286
|
+
<SectionCard title="Softphone"><p className="text-sm text-muted-foreground">Choose the rep above to take calls in this browser. A personal API key is the rep automatically; an organisation key is not a person.</p></SectionCard>
|
|
264
287
|
)}
|
|
265
288
|
{sessionId ? (
|
|
266
289
|
<>
|
|
@@ -282,11 +305,30 @@ function SessionTab(props: DialerScreenProps & { onCalls: (calls: LiveCall[]) =>
|
|
|
282
305
|
);
|
|
283
306
|
}
|
|
284
307
|
|
|
308
|
+
/** Who is calling, shown with a switcher when the rep was not passed in. Managers can pick a teammate; a personal key can only be itself. */
|
|
309
|
+
function RepSwitcher({ me, picked, onPick }: { me: ReturnType<typeof useCurrentMember>; picked: MemberOption | null; onPick: (m: MemberOption | null) => void }) {
|
|
310
|
+
const current = picked ?? me.member;
|
|
311
|
+
return (
|
|
312
|
+
<div className="flex flex-wrap items-center gap-2 text-sm" aria-label="Rep">
|
|
313
|
+
{me.loading ? <span className="text-muted-foreground">Finding your user...</span> : null}
|
|
314
|
+
{current ? <span>Calling as {current.name}{current.id === me.userId ? " (you)" : ""}</span> : null}
|
|
315
|
+
<MemberPicker label={current ? "Switch rep" : "Rep"} value={current?.id ?? null} onChange={onPick} placeholder="Choose who is calling" />
|
|
316
|
+
{picked && me.member && picked.id !== me.userId ? <Button size="sm" variant="ghost" onClick={() => onPick(null)}>Use me</Button> : null}
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
285
321
|
export function DialerScreen(props: DialerScreenProps) {
|
|
286
322
|
const [tab, setTab] = useUrlState("tab", "session");
|
|
287
323
|
const [, setView] = useUrlState("view");
|
|
288
324
|
const [, setSession] = useUrlState("session");
|
|
289
325
|
const [calls, setCalls] = useState<LiveCall[]>([]);
|
|
326
|
+
// The rep: a passed userId wins (BC). Otherwise the signed-in team member, or a teammate picked in the switcher.
|
|
327
|
+
const me = useCurrentMember({ enabled: !props.userId });
|
|
328
|
+
const [picked, setPicked] = useState<MemberOption | null>(null);
|
|
329
|
+
const rep = props.userId ? null : (picked ?? me.member);
|
|
330
|
+
const userId = props.userId ?? rep?.id;
|
|
331
|
+
const userEmail = props.userId ? props.userEmail : (rep ? (rep.email ?? undefined) : props.userEmail);
|
|
290
332
|
const active = TABS.some((t) => t.id === tab) ? tab : "session";
|
|
291
333
|
// The header counters describe the Session tab's calls; drop them when it unmounts so they are never stale.
|
|
292
334
|
useEffect(() => { if (active !== "session") setCalls([]); }, [active]);
|
|
@@ -305,15 +347,15 @@ export function DialerScreen(props: DialerScreenProps) {
|
|
|
305
347
|
title="Dialer"
|
|
306
348
|
description="Call a list batch by batch, take calls in the browser and record every outcome."
|
|
307
349
|
icon={<Phone className="size-5" />}
|
|
308
|
-
actions={props.toolbarActions}
|
|
350
|
+
actions={props.userId ? props.toolbarActions : <><RepSwitcher me={me} picked={picked} onPick={setPicked} />{props.toolbarActions}</>}
|
|
309
351
|
tabs={TABS}
|
|
310
352
|
activeTab={active}
|
|
311
353
|
onTabChange={(id) => setTab(id)}
|
|
312
354
|
stats={stats}
|
|
313
355
|
>
|
|
314
|
-
{active === "session" ? <SessionTab {...props} onCalls={setCalls} /> : null}
|
|
356
|
+
{active === "session" ? <SessionTab {...props} userId={userId} userEmail={userEmail} onCalls={setCalls} /> : null}
|
|
315
357
|
{active === "sessions" ? <SessionsTab userEmail={props.userEmail} onWork={(id) => { setSession(id); setView(null); setTab("session"); }} /> : null}
|
|
316
|
-
{active === "history" ? <CallHistory userEmail={props.userEmail} /> : null}
|
|
358
|
+
{active === "history" ? <CallHistory userEmail={props.userEmail ?? userEmail} /> : null}
|
|
317
359
|
</ScreenLayout>
|
|
318
360
|
);
|
|
319
361
|
}
|
|
@@ -54,7 +54,7 @@ export function DispositionPanel({ call, onSaved, className }: DispositionPanelP
|
|
|
54
54
|
return (
|
|
55
55
|
<section className={`space-y-3 rounded-lg border p-4 ${className ?? ""}`} aria-label="Call outcome">
|
|
56
56
|
<header>
|
|
57
|
-
<h3 className="text-sm font-
|
|
57
|
+
<h3 className="text-sm font-medium">How did the call with {contactName(call)} go?</h3>
|
|
58
58
|
{call.disposition ? <p className="text-xs text-muted-foreground">Currently: {call.disposition}</p> : null}
|
|
59
59
|
</header>
|
|
60
60
|
<div role="radiogroup" aria-label="Outcome" className="flex flex-wrap gap-2">
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RecordCallsTab: one contact's call history, newest first, with a "Call" button that places one confirmed call through the
|
|
5
|
+
* dialer. Use it as an `extraDetailTabs` entry of ContactsScreen:
|
|
6
|
+
* { id: "calls", label: "Calls", render: (c) => <RecordCallsTab contact={{ id: c.id, first_name: c.first_name, last_name: c.last_name, email: c.work_email, mobile_phone: c.mobile_phone, direct_phone: c.direct_phone }} onOpenCall={(room) => router.push(`/calls?id=${room}`)} /> }
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. listCallsForContact GET /voice/dialer/calls?contact_id=&limit= (read; 25 at first, "Show more" asks for up to 100, the API maximum)
|
|
10
|
+
* 2. CallContactButton: getMyTeamMember, listDialerSessions, listDialerNumbers, manualDial (external: confirmed in its dialog)
|
|
11
|
+
* The API has no offset, so past 100 calls the tab says so instead of cutting the list silently.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
16
|
+
import { PhoneCall } from "lucide-react";
|
|
17
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
18
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
19
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
20
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
22
|
+
import { CallContactButton } from "./call-contact-button";
|
|
23
|
+
import type { CallableContact } from "./call-contact-button";
|
|
24
|
+
import { dataOf, formatDuration, type CallSummary } from "../lib/dialer-types";
|
|
25
|
+
import { outcomeTone } from "./call-log-screen";
|
|
26
|
+
|
|
27
|
+
/** The most calls one read can return. */
|
|
28
|
+
export const CALLS_TAB_API_MAX = 100;
|
|
29
|
+
|
|
30
|
+
function when(iso?: string | null): string {
|
|
31
|
+
const d = iso ? new Date(iso) : null;
|
|
32
|
+
return d && !Number.isNaN(d.getTime()) ? d.toLocaleString(undefined, { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" }) : "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RecordCallsTabProps {
|
|
36
|
+
/** The contact: `id` scopes the history; the name and numbers feed the call button. */
|
|
37
|
+
contact: CallableContact & { id: string | number };
|
|
38
|
+
/** Route to the call in your calls screen; receives the call's room name. Without it the rows link to `${callsPath}?id=<room name>`. */
|
|
39
|
+
onOpenCall?: (roomName: string) => void;
|
|
40
|
+
/** Path of your calls screen for the plain links. Default "/calls". */
|
|
41
|
+
callsPath?: string;
|
|
42
|
+
/** Calls to read first. Default 25. */
|
|
43
|
+
pageSize?: number;
|
|
44
|
+
/** Pass the rep's softphone identity through to the call button (see CallContactButton). */
|
|
45
|
+
userId?: string;
|
|
46
|
+
userEmail?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function RecordCallsTab({ contact, onOpenCall, callsPath = "/calls", pageSize = 25, userId, userEmail }: RecordCallsTabProps) {
|
|
50
|
+
const [limit, setLimit] = useState(Math.min(pageSize, CALLS_TAB_API_MAX));
|
|
51
|
+
const contactId = String(contact.id);
|
|
52
|
+
const q = useG8Query(
|
|
53
|
+
async (signal) => dataOf<{ calls?: CallSummary[] }>(await g8Get("/voice/dialer/calls", { contact_id: contactId, limit }, { signal })).calls ?? [],
|
|
54
|
+
[contactId, limit],
|
|
55
|
+
);
|
|
56
|
+
const call = <CallContactButton contact={contact} userId={userId} userEmail={userEmail} onCalled={() => void q.refetch()} />;
|
|
57
|
+
if (q.error) return <ApiError error={q.error} title="Could not load calls" onRetry={() => void q.refetch()} />;
|
|
58
|
+
if (!q.data) return <Skeleton className="h-24 w-full" aria-label="Loading calls" />;
|
|
59
|
+
const calls = q.data;
|
|
60
|
+
const full = calls.length >= limit;
|
|
61
|
+
return (
|
|
62
|
+
<div className="space-y-3">
|
|
63
|
+
<div className="flex items-center justify-between gap-2">
|
|
64
|
+
<p className="text-sm text-muted-foreground">{calls.length === 1 ? "1 call" : `${calls.length.toLocaleString()}${full ? "+" : ""} calls`}</p>
|
|
65
|
+
{call}
|
|
66
|
+
</div>
|
|
67
|
+
{calls.length === 0 ? (
|
|
68
|
+
<EmptyState compact icon={<PhoneCall className="size-8" />} title="No calls yet" description="Calls the dialer places to this contact show up here." />
|
|
69
|
+
) : (
|
|
70
|
+
<ul className="divide-y rounded-md border" aria-label="Calls">
|
|
71
|
+
{calls.map((c) => (
|
|
72
|
+
<li key={c.room_name}>
|
|
73
|
+
<a
|
|
74
|
+
href={`${callsPath}?id=${encodeURIComponent(c.room_name)}`}
|
|
75
|
+
className="flex items-center justify-between gap-3 p-3 text-sm hover:bg-muted"
|
|
76
|
+
onClick={(e) => {
|
|
77
|
+
if (!onOpenCall || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
onOpenCall(c.room_name);
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<span className="min-w-0">
|
|
83
|
+
<span className="block font-medium">{when(c.created_at) || "Call"}</span>
|
|
84
|
+
<span className="block truncate text-xs text-muted-foreground">{[c.to, formatDuration(c.call_duration), c.summary ?? (c.has_transcript ? "Transcript available" : null)].filter(Boolean).join(" · ")}</span>
|
|
85
|
+
</span>
|
|
86
|
+
<StatusPill tone={outcomeTone(c.disposition)}>{c.disposition ? c.disposition.replace(/_/g, " ") : "no outcome"}</StatusPill>
|
|
87
|
+
</a>
|
|
88
|
+
</li>
|
|
89
|
+
))}
|
|
90
|
+
</ul>
|
|
91
|
+
)}
|
|
92
|
+
{full && limit < CALLS_TAB_API_MAX ? (
|
|
93
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
94
|
+
<span role="status">Showing {calls.length.toLocaleString()} calls</span>
|
|
95
|
+
<Button size="sm" variant="outline" onClick={() => setLimit(CALLS_TAB_API_MAX)}>Show more</Button>
|
|
96
|
+
</div>
|
|
97
|
+
) : null}
|
|
98
|
+
{full && limit >= CALLS_TAB_API_MAX ? <p role="note" className="text-xs text-muted-foreground">Showing the {CALLS_TAB_API_MAX} most recent calls. Older calls are in the call log by rep or session.</p> : null}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
* SessionStartForm: create a DRAFT dialer session for a list. A draft dials NOBODY; dialing starts only when
|
|
5
5
|
* CallQueue's "Dial next batch" is confirmed.
|
|
6
6
|
*
|
|
7
|
+
* No ids to type: the rep defaults to the signed-in team member (`useCurrentMember`, GET /team-members/me) and can be
|
|
8
|
+
* switched with a MemberPicker; the list is picked with a ListPicker. A `userId` / `listId` prop still wins, and the
|
|
9
|
+
* raw id fields stay under "Use a ... id instead" for a key that is not a person (an organisation key).
|
|
10
|
+
*
|
|
7
11
|
* SDK operations, in order:
|
|
12
|
+
* 0. GET /team-members/me (teamMembers.getMyTeamMember) read - only without `userId`: the rep is you
|
|
8
13
|
* 1. GET /voice/dialer/numbers (voice.listDialerNumbers) read - caller IDs and today's usage
|
|
9
14
|
* 2. POST /dialer/sessions/draft (dialer.createSessionDraft) write - safe: resolves the list, dials no one
|
|
15
|
+
* A personal key may only name itself as the rep: naming someone else answers 403, which the form explains.
|
|
10
16
|
* Calling the draft twice for the same list returns the existing live session instead of making a second one.
|
|
11
17
|
*/
|
|
12
18
|
import { useState } from "react";
|
|
@@ -17,6 +23,10 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
17
23
|
import { useDialerNumbers } from "../hooks/use-dialer-numbers";
|
|
18
24
|
import { dataOf, type DialerMode } from "../lib/dialer-types";
|
|
19
25
|
import { useBrand } from "@/g8/hooks/use-brand";
|
|
26
|
+
import { useCurrentMember } from "@/g8/pickers/hooks/use-current-member";
|
|
27
|
+
import { MemberPicker } from "@/g8/pickers/components/member-picker";
|
|
28
|
+
import type { MemberOption } from "@/g8/pickers/components/member-picker";
|
|
29
|
+
import { ListPicker } from "@/g8/lists/components/list-picker";
|
|
20
30
|
|
|
21
31
|
export interface SessionStartFormProps {
|
|
22
32
|
/** The rep these calls are attributed to. An organisation API key has no user of its own, so you name one. */
|
|
@@ -34,12 +44,22 @@ export function SessionStartForm({ userId, userEmail, listId: initialListId = ""
|
|
|
34
44
|
const brand = useBrand();
|
|
35
45
|
const { numbers, loading, error, refetch } = useDialerNumbers();
|
|
36
46
|
const [listId, setListId] = useState(initialListId);
|
|
47
|
+
const [pickedTitle, setPickedTitle] = useState("");
|
|
37
48
|
const [typedUserId, setTypedUserId] = useState("");
|
|
38
|
-
const
|
|
49
|
+
const [refused, setRefused] = useState(false);
|
|
50
|
+
// With a passed userId nobody is looked up, until a 403 needs to know whether the rep is someone else.
|
|
51
|
+
const me = useCurrentMember({ enabled: !userId || refused });
|
|
52
|
+
const [picked, setPicked] = useState<MemberOption | null>(null);
|
|
53
|
+
// Who the session is for: a passed userId, else a typed id, else the chosen teammate, else you. user_id and user_email
|
|
54
|
+
// always describe the same person.
|
|
55
|
+
const resolved = userId || typedUserId.trim() ? null : (picked ?? me.member);
|
|
56
|
+
const repId = userId || typedUserId.trim() || resolved?.id || "";
|
|
57
|
+
const repEmail = resolved ? (resolved.email ?? undefined) : userEmail;
|
|
39
58
|
const [fromPhone, setFromPhone] = useState("");
|
|
40
59
|
const [mode, setMode] = useState<DialerMode>("power");
|
|
41
60
|
const [parallelCount, setParallelCount] = useState(3);
|
|
42
61
|
const [name, setName] = useState(listTitle ? `Dialing ${listTitle}` : "");
|
|
62
|
+
const title = listTitle ?? pickedTitle;
|
|
43
63
|
|
|
44
64
|
const chosenPhone = fromPhone || numbers[0]?.phone_number || "";
|
|
45
65
|
const create = useG8Mutation(
|
|
@@ -47,9 +67,9 @@ export function SessionStartForm({ userId, userEmail, listId: initialListId = ""
|
|
|
47
67
|
const count = mode === "parallel" ? Math.min(10, Math.max(2, Math.round(parallelCount) || 2)) : 1;
|
|
48
68
|
const env = await g8Post("/dialer/sessions/draft", {
|
|
49
69
|
user_id: repId,
|
|
50
|
-
user_email:
|
|
70
|
+
user_email: repEmail,
|
|
51
71
|
list_id: listId,
|
|
52
|
-
list_title:
|
|
72
|
+
list_title: title || undefined,
|
|
53
73
|
name: name || undefined,
|
|
54
74
|
from_phone: chosenPhone || undefined,
|
|
55
75
|
dialing_mode: mode,
|
|
@@ -60,21 +80,41 @@ export function SessionStartForm({ userId, userEmail, listId: initialListId = ""
|
|
|
60
80
|
if (!sessionId) throw new Error(`${brand.linkLabel} did not return a session id`);
|
|
61
81
|
return { sessionId, mode, parallelCount: count, fromPhone: chosenPhone };
|
|
62
82
|
},
|
|
63
|
-
{ onSuccess: onCreated },
|
|
83
|
+
{ onSuccess: onCreated, onError: (e) => setRefused((e as { status?: number } | undefined)?.status === 403) },
|
|
64
84
|
);
|
|
65
85
|
|
|
66
86
|
return (
|
|
67
87
|
<form className={`space-y-3 rounded-lg border p-4 ${className ?? ""}`} onSubmit={(e) => { e.preventDefault(); if (listId && repId) void create.mutate(undefined); }}>
|
|
68
|
-
<h3 className="text-sm font-
|
|
88
|
+
<h3 className="text-sm font-medium">Start a dialing session</h3>
|
|
69
89
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
70
|
-
<div className="space-y-1">
|
|
71
|
-
<
|
|
72
|
-
|
|
90
|
+
<div className="space-y-1 sm:col-span-2">
|
|
91
|
+
<span className="text-sm font-medium leading-none">List</span>
|
|
92
|
+
{initialListId ? (
|
|
93
|
+
<p className="text-sm">{title || `List ${initialListId}`}</p>
|
|
94
|
+
) : (
|
|
95
|
+
<ListPicker type="contacts" allowCreate={false} value={Number(listId) || null} onChange={(l) => { setListId(String(l.id)); setPickedTitle(l.title); setName((n) => n || `Dialing ${l.title}`); }} />
|
|
96
|
+
)}
|
|
97
|
+
<details className="text-sm">
|
|
98
|
+
<summary className="cursor-pointer text-muted-foreground">Use a list id instead</summary>
|
|
99
|
+
<div className="mt-1 space-y-1">
|
|
100
|
+
<Label htmlFor="ds-list">List id</Label>
|
|
101
|
+
<Input id="ds-list" value={listId} onChange={(e) => setListId(e.target.value)} placeholder="From your lists" />
|
|
102
|
+
</div>
|
|
103
|
+
</details>
|
|
73
104
|
</div>
|
|
74
105
|
{userId ? null : (
|
|
75
|
-
<div className="space-y-1">
|
|
76
|
-
<
|
|
77
|
-
<
|
|
106
|
+
<div className="space-y-1 sm:col-span-2">
|
|
107
|
+
<span className="text-sm font-medium leading-none">Rep</span>
|
|
108
|
+
{me.loading ? <p className="text-sm text-muted-foreground">Finding your user...</p> : null}
|
|
109
|
+
{resolved ? <p className="text-sm">Calling as {resolved.name}{resolved.id === me.userId ? " (you)" : ""}</p> : null}
|
|
110
|
+
<MemberPicker label={resolved ? "Switch rep" : "Rep"} value={resolved?.id ?? null} onChange={(m) => { setPicked(m); setTypedUserId(""); }} placeholder="Choose who is calling" />
|
|
111
|
+
<details className="text-sm">
|
|
112
|
+
<summary className="cursor-pointer text-muted-foreground">Use a user id instead</summary>
|
|
113
|
+
<div className="mt-1 space-y-1">
|
|
114
|
+
<Label htmlFor="ds-user">Rep user id</Label>
|
|
115
|
+
<Input id="ds-user" value={typedUserId} onChange={(e) => setTypedUserId(e.target.value)} placeholder={`${brand.linkLabel} user id`} />
|
|
116
|
+
</div>
|
|
117
|
+
</details>
|
|
78
118
|
</div>
|
|
79
119
|
)}
|
|
80
120
|
<div className="space-y-1">
|
|
@@ -105,6 +145,13 @@ export function SessionStartForm({ userId, userEmail, listId: initialListId = ""
|
|
|
105
145
|
</div>
|
|
106
146
|
{error ? <ApiError error={error} title="Could not load caller IDs" onRetry={() => void refetch()} /> : null}
|
|
107
147
|
{create.error ? <ApiError error={create.error} title="Could not create the session" /> : null}
|
|
148
|
+
{(create.error as { status?: number } | undefined)?.status === 403 ? (
|
|
149
|
+
<p role="alert" className="text-sm text-muted-foreground">
|
|
150
|
+
{me.userId && repId && repId !== me.userId
|
|
151
|
+
? "A personal API key can only act as its own user, so this session cannot be started for someone else. Switch the rep back to yourself."
|
|
152
|
+
: "This API key is not allowed to start dialer sessions. Check that it has the voice scopes, or ask an admin."}
|
|
153
|
+
</p>
|
|
154
|
+
) : null}
|
|
108
155
|
<Button type="submit" disabled={!listId || !repId || create.loading} loading={create.loading}>
|
|
109
156
|
Create session (dials no one yet)
|
|
110
157
|
</Button>
|
|
@@ -93,7 +93,7 @@ export function Softphone({ userId, transport, conferenceName, joinMuted, onStat
|
|
|
93
93
|
return (
|
|
94
94
|
<section className={`space-y-3 rounded-lg border p-4 ${className ?? ""}`} aria-label="Softphone">
|
|
95
95
|
<div className="flex items-center justify-between">
|
|
96
|
-
<h3 className="text-sm font-
|
|
96
|
+
<h3 className="text-sm font-medium">Softphone</h3>
|
|
97
97
|
<span role="status" className="text-sm text-muted-foreground">{label}</span>
|
|
98
98
|
</div>
|
|
99
99
|
{transportError ? <p role="alert" className="text-sm text-destructive">{transportError}</p> : null}
|
|
@@ -12,7 +12,7 @@ tick contacts, run a bulk enrichment with preflight, confirmed price, progress a
|
|
|
12
12
|
or verify its email), **Look up** (one person or company, and email verification), **Pipelines** (a list's saved
|
|
13
13
|
provider waterfalls: create, delete) and **Providers** (graph8-funded vs your own key, credits per action; keys are
|
|
14
14
|
added in graph8 settings, so it links out). Every paid step goes through the confirmation dialog.
|
|
15
|
-
Also a **Jobs** tab (`?job=`, `EnrichmentJobDetail`: live progress, counts, credits, provider errors, cancel; jobs started this session are listed,
|
|
15
|
+
Also a **Jobs** tab (`?job=`, `EnrichmentJobDetail`: live progress, counts, credits, provider errors, cancel; the jobs running now come from `GET /enrichment/active-jobs` and jobs started this session are listed, so no id has to be pasted; an older job can still be opened by id) and a pipeline detail (`?pipeline=`: steps, credential status, missing keys). `?view=full` opens any detail full-page.
|
|
16
16
|
Slots: `toolbarActions`, `renderDetailActions(contact)`, `extraDetailTabs`.
|
|
17
17
|
|
|
18
18
|
Prerequisites: `crm` and `lists` installed, proxy allowlist `enrichment`, `lists`, `contacts`, `companies`; provider keys and credits as below.
|
|
@@ -49,11 +49,20 @@ you can afford it; an under-funded run fails with 402.
|
|
|
49
49
|
Masked search results have no ids and cannot be enriched: save them to a list first (`search` block).
|
|
50
50
|
2. `POST /enrichment/preflight { list_id, contact_ids, mode: "waterfall", providers, target_field }` (free) ->
|
|
51
51
|
`contact_count`, `not_in_list`, `missing_credentials`, `warnings`, `credits_per_record`, `max_total_credits`.
|
|
52
|
-
3. The person confirms the estimate. `POST /enrichment/
|
|
53
|
-
|
|
52
|
+
3. The person confirms the estimate. `POST /enrichment/waterfall/configs { list_id, name, field: "CONTACT_WORK_EMAIL", providers }`
|
|
53
|
+
creates the list's pipeline for that field, or reuses the one it already has (`created: false`; the run then uses
|
|
54
|
+
ITS providers) -> `column_id`. Then `POST /enrichment/waterfall/enrich { column_id, list_id, record_ids }` -> 202
|
|
55
|
+
`{ job_id, status }`. A `status` of `"error"` (or a 502) means the job never started: show it as a failure, do not
|
|
56
|
+
poll. Providers must suit the field: phones take leadmagic, prospeo, apollo only (`FIELD_PROVIDERS`); the panel
|
|
57
|
+
resets the providers when the field changes. Before the confirm, the panel reads the list's pipelines
|
|
58
|
+
(`GET /enrichment/waterfall/configs?list_id`): when one already fills the field, the quote, the confirm and the run
|
|
59
|
+
use ITS providers (the API reuses it), and the panel says so instead of offering a provider editor.
|
|
60
|
+
`POST /enrichment/enrich` is not used: until the API fix of 2026-10-16 it dispatched the field name instead of a
|
|
61
|
+
pipeline and silently enriched nothing, so apps on older SDK copies should take this path.
|
|
54
62
|
4. Poll `GET /enrichment/jobs/{job_id}` (`useJobPoll`, backoff; there is no stream and no completion webhook) until
|
|
55
63
|
`status` is `completed`, `failed` or `cancelled`. It reports `completed/total`, `successful_enrichments`, `failed_enrichments`,
|
|
56
64
|
`total_credits_used`. If nothing was enriched, `?include_provider_errors=true` explains why (usually a missing provider key).
|
|
65
|
+
When the job ends the panel refreshes every cached `contacts` query and calls `onDone`, so tables and records reload.
|
|
57
66
|
5. `POST /enrichment/jobs/{job_id}/cancel` stops a running job; records already done keep their data and charge.
|
|
58
67
|
|
|
59
68
|
```tsx
|
|
@@ -97,7 +106,7 @@ Ask the person: who may spend credits (decide it in the proxy's `authorize`).
|
|
|
97
106
|
| File | Does |
|
|
98
107
|
|---|---|
|
|
99
108
|
| `recipes/lookup-and-verify.ts` | One person/company lookup and an email check |
|
|
100
|
-
| `recipes/bulk-enrich-and-poll.ts` | Preflight quote,
|
|
109
|
+
| `recipes/bulk-enrich-and-poll.ts` | Preflight quote, create or reuse the pipeline and run it, poll with backoff, read provider errors |
|
|
101
110
|
| `recipes/waterfall-pipeline.ts` | Provider readiness, create and list pipelines |
|
|
102
111
|
|
|
103
112
|
## Operations
|
|
@@ -107,11 +116,12 @@ Ask the person: who may spend credits (decide it in the proxy's `authorize`).
|
|
|
107
116
|
| lookupPerson / lookupCompany | POST /enrichment/lookup/person, /company | billable | EnrichButton |
|
|
108
117
|
| verifyEmail | POST /enrichment/verify-email | billable | EmailVerifyBadge |
|
|
109
118
|
| preflightEnrichment | POST /enrichment/preflight | read | BulkEnrichPanel |
|
|
110
|
-
| startEnrichment | POST /enrichment/enrich | billable |
|
|
119
|
+
| startEnrichment | POST /enrichment/enrich | billable | (not used by the components; see the flow) |
|
|
120
|
+
| runWaterfallPipeline | POST /enrichment/waterfall/enrich | billable | BulkEnrichPanel |
|
|
111
121
|
| getEnrichmentJob | GET /enrichment/jobs/{job_id} | read | BulkEnrichPanel |
|
|
112
122
|
| cancelEnrichmentJob | POST /enrichment/jobs/{job_id}/cancel | write | BulkEnrichPanel |
|
|
113
123
|
| listEnrichmentProviders | GET /enrichment/providers | read | BulkEnrichPanel, WaterfallConfig |
|
|
114
124
|
| listWaterfallConfigs | GET /enrichment/waterfall/configs | read | WaterfallConfig |
|
|
115
|
-
| createDefaultWaterfallConfig | POST /enrichment/waterfall/configs | billable | WaterfallConfig |
|
|
125
|
+
| createDefaultWaterfallConfig | POST /enrichment/waterfall/configs | billable | WaterfallConfig, BulkEnrichPanel |
|
|
116
126
|
| deleteWaterfallConfig | DELETE /enrichment/waterfall/configs/{column_id} | destructive | WaterfallConfig |
|
|
117
127
|
| updateContact / updateCompany | PATCH /contacts/{id}, /companies/{id} | write | EnrichButton (Apply) |
|