@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
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"finished EnrichmentScreen page"
|
|
12
12
|
],
|
|
13
13
|
"dependsOn": [
|
|
14
|
-
"lists"
|
|
14
|
+
"lists",
|
|
15
|
+
"pickers"
|
|
15
16
|
],
|
|
16
17
|
"operations": [
|
|
17
18
|
"cancel_enrichment_job_enrichment_jobs__job_id__cancel_post",
|
|
@@ -22,6 +23,8 @@
|
|
|
22
23
|
"get_company_companies__company_id__get",
|
|
23
24
|
"get_contact_contacts__contact_id__get",
|
|
24
25
|
"get_enrichment_job_enrichment_jobs__job_id__get",
|
|
26
|
+
"list_active_enrichment_jobs_enrichment_active_jobs_get",
|
|
27
|
+
"list_companies_companies_get",
|
|
25
28
|
"list_contact_fields_fields_get",
|
|
26
29
|
"list_contacts_contacts_get",
|
|
27
30
|
"list_enrichment_providers_enrichment_providers_get",
|
|
@@ -30,6 +33,7 @@
|
|
|
30
33
|
"lookup_company_enrichment_lookup_company_post",
|
|
31
34
|
"lookup_person_enrichment_lookup_person_post",
|
|
32
35
|
"preflight_enrichment_enrichment_preflight_post",
|
|
36
|
+
"run_waterfall_pipeline_enrichment_waterfall_enrich_post",
|
|
33
37
|
"start_enrichment_enrichment_enrich_post",
|
|
34
38
|
"update_company_companies__company_id__patch",
|
|
35
39
|
"update_contact_contacts__contact_id__patch",
|
|
@@ -68,11 +72,13 @@
|
|
|
68
72
|
{
|
|
69
73
|
"name": "BulkEnrichPanel",
|
|
70
74
|
"file": "components/bulk-enrich-panel.tsx",
|
|
71
|
-
"purpose": "Fill a field for many contacts of a list: free preflight, credit estimate in ConfirmSpendDialog,
|
|
75
|
+
"purpose": "Fill a field for many contacts of a list: free preflight, credit estimate in ConfirmSpendDialog, create or reuse the field's pipeline and run it, poll it with useJobPoll, cancel. With no props it is a whole page (pick a list, tick contacts).",
|
|
72
76
|
"operations": [
|
|
73
77
|
"list_enrichment_providers_enrichment_providers_get",
|
|
78
|
+
"list_waterfall_configs_enrichment_waterfall_configs_get",
|
|
74
79
|
"preflight_enrichment_enrichment_preflight_post",
|
|
75
|
-
"
|
|
80
|
+
"create_default_waterfall_config_enrichment_waterfall_configs_post",
|
|
81
|
+
"run_waterfall_pipeline_enrichment_waterfall_enrich_post",
|
|
76
82
|
"get_enrichment_job_enrichment_jobs__job_id__get",
|
|
77
83
|
"cancel_enrichment_job_enrichment_jobs__job_id__cancel_post",
|
|
78
84
|
"list_lists_lists_get",
|
|
@@ -108,8 +114,14 @@
|
|
|
108
114
|
"name": "EnrichmentScreen",
|
|
109
115
|
"file": "components/enrichment-screen.tsx",
|
|
110
116
|
"purpose": "Finished enrichment page: enrich a list, one-off lookups and email verification, pipelines and the provider catalog.",
|
|
111
|
-
"operations": [
|
|
112
|
-
|
|
117
|
+
"operations": [
|
|
118
|
+
"list_contacts_contacts_get",
|
|
119
|
+
"list_companies_companies_get",
|
|
120
|
+
"list_active_enrichment_jobs_enrichment_active_jobs_get"
|
|
121
|
+
],
|
|
122
|
+
"tiers": [
|
|
123
|
+
"read"
|
|
124
|
+
]
|
|
113
125
|
},
|
|
114
126
|
{
|
|
115
127
|
"name": "EnrichmentJobDetail",
|
|
@@ -150,7 +162,7 @@
|
|
|
150
162
|
{
|
|
151
163
|
"file": "recipes/bulk-enrich-and-poll.ts",
|
|
152
164
|
"title": "Enrich many contacts of a list and wait for the job",
|
|
153
|
-
"when": "Filling work emails or phones for a list. Order: preflight (free: counts, missing provider keys and the credit quote) -> a human approves the quote ->
|
|
165
|
+
"when": "Filling work emails or phones for a list. Order: preflight (free: counts, missing provider keys and the credit quote) -> a human approves the quote -> create or reuse the field's pipeline and run it -> poll it (the API has no streams or webhook for completion) -> read provider errors if nothing was written. Contact ids must be on the list; ids not on it are skipped and not charged. Never enrich contacts you only have as masked search results: save them to a list first."
|
|
154
166
|
},
|
|
155
167
|
{
|
|
156
168
|
"file": "recipes/waterfall-pipeline.ts",
|
|
@@ -201,7 +213,7 @@
|
|
|
201
213
|
"defaults": [
|
|
202
214
|
{
|
|
203
215
|
"decision": "Providers a bulk run tries",
|
|
204
|
-
"default": "prospeo then dropcontact
|
|
216
|
+
"default": "The API's own per field (work email: prospeo then dropcontact; phones: leadmagic, prospeo, apollo), kept to providers in the catalog that the field supports; editable in the panel. A list that already has a pipeline for the field runs with that pipeline's providers",
|
|
205
217
|
"askHuman": false
|
|
206
218
|
},
|
|
207
219
|
{
|
|
@@ -7,21 +7,31 @@
|
|
|
7
7
|
*
|
|
8
8
|
* SDK operations, in order:
|
|
9
9
|
* 1. listEnrichmentProviders GET /enrichment/providers (read) provider choices, graph8-funded vs own key
|
|
10
|
+
* 1b. listWaterfallConfigs GET /enrichment/waterfall/configs?list_id (read) the list's own pipeline for the field, if any:
|
|
11
|
+
* a run reuses it with ITS providers, so the quote and the confirm are priced with those, not the chosen ones.
|
|
10
12
|
* 2. preflightEnrichment POST /enrichment/preflight (read, free; nothing is written or charged)
|
|
11
13
|
* { list_id, contact_ids, mode: "waterfall", providers, target_field } -> contact_count, not_in_list,
|
|
12
14
|
* missing_credentials, warnings, credits_per_record, max_total_credits
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* After ConfirmSpendDialog (with `max_total_credits`, or 2 credits x contacts when the preflight could not price it;
|
|
16
|
+
* there is no balance read, so this is an estimate of the most it can charge):
|
|
17
|
+
* 3. createDefaultWaterfallConfig POST /enrichment/waterfall/configs { list_id, name, field, providers } (billable tier, free)
|
|
18
|
+
* Creates the list's pipeline for that field, or reuses the one it already has (`created: false`; it then runs
|
|
19
|
+
* with ITS providers). The column_id is kept, so a retry after step 4 failed does not create it again.
|
|
20
|
+
* 4. runWaterfallPipeline POST /enrichment/waterfall/enrich { column_id, list_id, record_ids } (billable) 202 { job_id, status }
|
|
21
|
+
* A 202 whose status is "error" (or a 502) means the job never started: shown as a failure, never polled.
|
|
22
|
+
* 5. getEnrichmentJob GET /enrichment/jobs/{job_id} (read) polled with useJobPoll (no streams), backoff;
|
|
17
23
|
* read once more with ?include_provider_errors=true when nothing was enriched or some records failed.
|
|
18
|
-
*
|
|
24
|
+
* 6. cancelEnrichmentJob POST /enrichment/jobs/{job_id}/cancel (write, free; records already done keep their data and charge)
|
|
25
|
+
*
|
|
26
|
+
* When the job ends, every cached `contacts` query is refreshed (and the page mode's ContactTable reloads) so the
|
|
27
|
+
* filled values show. The panel does not use POST /enrichment/enrich (startEnrichment): before the API fix of
|
|
28
|
+
* 2026-10-16 that route dispatched the field name instead of a pipeline and silently enriched nothing.
|
|
19
29
|
*
|
|
20
30
|
* The contacts must be on `listId`; ids not on it are skipped and not charged. Locked contacts are enriched
|
|
21
31
|
* like any other once they sit in a list.
|
|
22
32
|
*/
|
|
23
33
|
|
|
24
|
-
import { useState } from "react";
|
|
34
|
+
import { useRef, useState } from "react";
|
|
25
35
|
import { Button } from "@graph8/sdk/ui";
|
|
26
36
|
import { ApiError } from "@/g8/components/api-error";
|
|
27
37
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
@@ -29,19 +39,23 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
29
39
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
30
40
|
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
31
41
|
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
42
|
+
import { g8QueryCache } from "@/g8/lib/g8-cache";
|
|
32
43
|
import { ContactTable } from "@/g8/crm/components/contact-table";
|
|
33
44
|
import { ListPicker } from "@/g8/lists/components/list-picker";
|
|
34
45
|
import { plural } from "@/g8/lib/plural";
|
|
35
46
|
import { useProviders } from "../hooks/use-providers";
|
|
36
|
-
import { CREDITS_PER_RECORD_ESTIMATE, TARGET_FIELDS, estimateCredits, isJobTerminal } from "../lib/enrichment-types";
|
|
37
|
-
import type { EnrichJob, Preflight, TargetField } from "../lib/enrichment-types";
|
|
47
|
+
import { CREDITS_PER_RECORD_ESTIMATE, FIELD_PROVIDERS, TARGET_FIELDS, estimateCredits, isJobTerminal, providersForField } from "../lib/enrichment-types";
|
|
48
|
+
import type { EnrichJob, Preflight, TargetField, WaterfallConfigSummary, WaterfallPipelineResult } from "../lib/enrichment-types";
|
|
38
49
|
import { useBrand } from "@/g8/hooks/use-brand";
|
|
39
50
|
|
|
40
51
|
export interface BulkEnrichPanelProps {
|
|
41
52
|
/** With `listId` and `contactIds` it is just the enrichment panel. Omit them and it is a whole page: pick a list, tick contacts, enrich. */
|
|
42
53
|
listId?: number;
|
|
43
54
|
contactIds?: number[];
|
|
44
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Providers tried, in order, when the person has not chosen. Kept to the providers the field supports. Default: the
|
|
57
|
+
* API's own per field (work email: prospeo, dropcontact; phones: leadmagic, prospeo, apollo).
|
|
58
|
+
*/
|
|
45
59
|
defaultProviders?: string[];
|
|
46
60
|
defaultField?: TargetField;
|
|
47
61
|
/** Called when the job reached a final state. */
|
|
@@ -51,37 +65,64 @@ export interface BulkEnrichPanelProps {
|
|
|
51
65
|
className?: string;
|
|
52
66
|
}
|
|
53
67
|
|
|
54
|
-
|
|
68
|
+
type StartedRun = { job: EnrichJob; pipeline: WaterfallPipelineResult; asked: string[] };
|
|
55
69
|
|
|
56
|
-
function BulkEnrichCore({ listId, contactIds, defaultProviders
|
|
70
|
+
function BulkEnrichCore({ listId, contactIds, defaultProviders, defaultField = "work_email", onDone, onStarted, className }: BulkEnrichPanelProps & { listId: number; contactIds: number[] }) {
|
|
57
71
|
const brand = useBrand();
|
|
58
72
|
const [field, setField] = useState<TargetField>(defaultField);
|
|
59
73
|
const [chosen, setChosen] = useState<string[] | null>(null);
|
|
60
74
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
61
75
|
const [jobId, setJobId] = useState<string | null>(null);
|
|
76
|
+
const [run, setRun] = useState<StartedRun | null>(null);
|
|
77
|
+
// The pipeline step 3 created or reused, kept per list/field/providers so a retry does not create it twice.
|
|
78
|
+
const pipelineRef = useRef<{ key: string; pipeline: WaterfallPipelineResult } | null>(null);
|
|
62
79
|
const catalog = useProviders();
|
|
63
80
|
|
|
64
81
|
const known = new Set(catalog.providers.map((p) => p.provider));
|
|
65
|
-
const providers = chosen ?? defaultProviders.filter((p) => known.size === 0 || known.has(p));
|
|
82
|
+
const providers = chosen ?? providersForField(field, defaultProviders).filter((p) => known.size === 0 || known.has(p));
|
|
83
|
+
const offered = catalog.providers.filter((p) => FIELD_PROVIDERS[field].includes(p.provider) && !providers.includes(p.provider));
|
|
66
84
|
const column = TARGET_FIELDS.find((f) => f.value === field)!;
|
|
67
85
|
const ids = [...new Set(contactIds)];
|
|
68
86
|
|
|
87
|
+
// The list's own pipeline for this field wins over the chosen providers (the API reuses it), so price that one.
|
|
88
|
+
const saved = useG8Query(
|
|
89
|
+
async (signal) => (await g8Get<{ configs: WaterfallConfigSummary[] }>("/enrichment/waterfall/configs", { list_id: listId }, { signal })).data.configs ?? [],
|
|
90
|
+
[listId],
|
|
91
|
+
{ enabled: !jobId },
|
|
92
|
+
);
|
|
93
|
+
const existing = saved.data?.find((c) => c.field_to_enrich === column.target && c.providers.length > 0) ?? null;
|
|
94
|
+
const runProviders = existing ? existing.providers : providers;
|
|
95
|
+
|
|
69
96
|
const preflight = useG8Query(
|
|
70
|
-
async (signal) => (await g8Post<Preflight>("/enrichment/preflight", { list_id: listId, contact_ids: ids, mode: "waterfall", providers, target_field: column.target }, { signal })).data,
|
|
71
|
-
[listId, ids.join(","),
|
|
97
|
+
async (signal) => (await g8Post<Preflight>("/enrichment/preflight", { list_id: listId, contact_ids: ids, mode: "waterfall", providers: runProviders, target_field: column.target }, { signal })).data,
|
|
98
|
+
[listId, ids.join(","), runProviders.join(","), field],
|
|
72
99
|
{ enabled: ids.length > 0 && !jobId },
|
|
73
100
|
);
|
|
74
101
|
const quote = preflight.data;
|
|
75
102
|
const credits = estimateCredits(quote, ids.length);
|
|
76
103
|
const runnable = quote ? quote.contact_count - quote.not_in_list.length : ids.length;
|
|
77
104
|
|
|
78
|
-
const start = useG8Mutation(()
|
|
79
|
-
|
|
105
|
+
const start = useG8Mutation(async (): Promise<StartedRun> => {
|
|
106
|
+
const key = `${listId}|${column.target}|${providers.join(",")}`;
|
|
107
|
+
let pipeline = pipelineRef.current?.key === key ? pipelineRef.current.pipeline : null;
|
|
108
|
+
if (!pipeline) {
|
|
109
|
+
pipeline = (await g8Post<WaterfallPipelineResult>("/enrichment/waterfall/configs", { list_id: listId, name: `${column.label} enrichment`, field: column.target, providers })).data;
|
|
110
|
+
pipelineRef.current = { key, pipeline };
|
|
111
|
+
}
|
|
112
|
+
const job = (await g8Post<EnrichJob>("/enrichment/waterfall/enrich", { column_id: pipeline.column_id, list_id: listId, record_ids: ids })).data;
|
|
113
|
+
if (!job?.job_id || String(job.status).toLowerCase() === "error") throw new Error("The enrichment did not start, so nothing was charged. Try again in a minute.");
|
|
114
|
+
return { job, pipeline, asked: providers };
|
|
115
|
+
}, {
|
|
116
|
+
onSuccess: (res) => { setConfirmOpen(false); setRun(res); setJobId(res.job.job_id); onStarted?.(res.job.job_id); },
|
|
80
117
|
});
|
|
81
118
|
const job = useJobPoll<EnrichJob>(
|
|
82
119
|
async (signal) => (await g8Get<EnrichJob>(`/enrichment/jobs/${jobId}`, undefined, { signal })).data,
|
|
83
|
-
{
|
|
120
|
+
{
|
|
121
|
+
enabled: jobId != null, intervalMs: 2000, isTerminal: (j) => isJobTerminal(j.status),
|
|
122
|
+
onDone: (j) => { void g8QueryCache.invalidate(["contacts"]); onDone?.(j); },
|
|
123
|
+
},
|
|
84
124
|
);
|
|
125
|
+
const reused = run && !run.pipeline.created && run.pipeline.providers?.length && run.pipeline.providers.join() !== run.asked.join() ? run.pipeline.providers : null;
|
|
85
126
|
const cancel = useG8Mutation(() => g8Post(`/enrichment/jobs/${jobId}/cancel`, {}));
|
|
86
127
|
const finished = job.status;
|
|
87
128
|
const nothingWritten = job.done && ((finished?.successful_enrichments ?? 0) === 0 || (finished?.failed_enrichments ?? 0) > 0);
|
|
@@ -106,9 +147,10 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
106
147
|
const pct = total ? Math.round(((j?.completed ?? 0) / total) * 100) : 0;
|
|
107
148
|
return (
|
|
108
149
|
<div className={`flex flex-col gap-3 ${className ?? ""}`} role="status">
|
|
109
|
-
<h3 className="text-base font-
|
|
150
|
+
<h3 className="text-base font-medium">{job.done ? (j?.status === "completed" ? "Enrichment finished" : `Enrichment ${j?.status}`) : "Enriching..."}</h3>
|
|
110
151
|
<div className="h-2 overflow-hidden rounded bg-muted" aria-label="Progress"><div className="h-full bg-primary transition-all" style={{ width: `${job.done ? 100 : pct}%` }} /></div>
|
|
111
152
|
<p className="text-sm">{j?.completed ?? 0} of {total} contacts processed{j?.successful_enrichments != null ? `, ${j.successful_enrichments} enriched, ${j.failed_enrichments ?? 0} failed` : ""}.{j?.total_credits_used != null ? ` ${j.total_credits_used} credits used.` : ""}</p>
|
|
153
|
+
{reused ? <p className="text-xs text-muted-foreground">This list already had a {column.label.toLowerCase()} pipeline, so it ran with its providers: {reused.join(" > ")}.</p> : null}
|
|
112
154
|
{j?.warnings?.map((w) => <p key={w} className="text-xs text-muted-foreground">{w}</p>)}
|
|
113
155
|
{job.error ? <ApiError error={job.error} title="Could not read the job" onRetry={() => job.start()} /> : null}
|
|
114
156
|
{errors.data?.length ? (
|
|
@@ -119,7 +161,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
119
161
|
{cancel.error ? <ApiError error={cancel.error} title="Could not cancel" /> : null}
|
|
120
162
|
<div className="flex gap-2">
|
|
121
163
|
{!job.done ? <Button variant="outline" size="sm" loading={cancel.loading} onClick={() => void cancel.mutate(undefined)}>Cancel</Button> : null}
|
|
122
|
-
{job.done ? <Button size="sm" variant="outline" onClick={() => { setJobId(null); job.stop(); }}>Enrich more</Button> : null}
|
|
164
|
+
{job.done ? <Button size="sm" variant="outline" onClick={() => { setJobId(null); setRun(null); job.stop(); }}>Enrich more</Button> : null}
|
|
123
165
|
</div>
|
|
124
166
|
</div>
|
|
125
167
|
);
|
|
@@ -129,7 +171,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
129
171
|
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
130
172
|
<div className="flex flex-wrap items-center gap-3">
|
|
131
173
|
<label className="flex items-center gap-2 text-sm">Fill
|
|
132
|
-
<select aria-label="Field to fill" className="h-9 rounded-md border bg-background px-2" value={field} onChange={(e) => setField(e.target.value as TargetField)}>
|
|
174
|
+
<select aria-label="Field to fill" className="h-9 rounded-md border bg-background px-2" value={field} onChange={(e) => { setField(e.target.value as TargetField); setChosen(null); }}>
|
|
133
175
|
{TARGET_FIELDS.map((f) => <option key={f.value} value={f.value}>{f.label}</option>)}
|
|
134
176
|
</select>
|
|
135
177
|
</label>
|
|
@@ -139,7 +181,11 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
139
181
|
<fieldset className="flex flex-col gap-1">
|
|
140
182
|
<legend className="mb-1 text-sm font-medium">Providers, tried in this order</legend>
|
|
141
183
|
{catalog.error ? <ApiError error={catalog.error} title="Could not load providers" onRetry={() => void catalog.refetch()} /> : null}
|
|
142
|
-
{
|
|
184
|
+
{existing ? (
|
|
185
|
+
<p className="text-sm text-muted-foreground" data-testid="saved-pipeline">
|
|
186
|
+
This list already has a {column.label.toLowerCase()} pipeline ("{existing.name}"), so a run uses its providers: {existing.providers.join(" > ")}. Delete it under Pipelines to choose others.
|
|
187
|
+
</p>
|
|
188
|
+
) : providers.map((p, i) => (
|
|
143
189
|
<div key={p} className="flex items-center gap-2 text-sm">
|
|
144
190
|
<span className="w-6 text-muted-foreground">{i + 1}.</span><span className="w-32 font-medium">{p}</span>
|
|
145
191
|
<Button variant="ghost" size="sm" aria-label={`Move ${p} up`} disabled={i === 0} onClick={() => move(p, -1)}>Up</Button>
|
|
@@ -148,7 +194,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
148
194
|
</div>
|
|
149
195
|
))}
|
|
150
196
|
<div className="flex flex-wrap gap-1">
|
|
151
|
-
{
|
|
197
|
+
{(existing ? [] : offered).map((p) => (
|
|
152
198
|
<Button key={p.provider} variant="outline" size="sm" onClick={() => toggleProvider(p.provider)}>Add {p.provider}{p.byok_configured ? " (your key)" : p.system_funded ? "" : " (needs a key)"}</Button>
|
|
153
199
|
))}
|
|
154
200
|
</div>
|
|
@@ -159,14 +205,14 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
159
205
|
<div className="rounded-md border p-3 text-sm" aria-label="Preflight">
|
|
160
206
|
<p><strong>{runnable.toLocaleString()}</strong> of {quote.contact_count.toLocaleString()} contacts will be enriched{quote.not_in_list.length ? `; ${quote.not_in_list.length} are not on this list and are skipped (not charged)` : ""}{quote.duplicates_removed ? `; ${quote.duplicates_removed} duplicates removed` : ""}.</p>
|
|
161
207
|
<p className="mt-1">{quote.max_total_credits != null ? `At most ${quote.max_total_credits.toLocaleString()} credits (${quote.credits_per_record} per record).` : `About ${credits.toLocaleString()} credits (${CREDITS_PER_RECORD_ESTIMATE} per record, the API's flat estimate).`} Records that already have a value or find nothing cost less.</p>
|
|
162
|
-
{quote.missing_credentials.length ? <p role="alert" className="mt-1 text-
|
|
208
|
+
{quote.missing_credentials.length ? <p role="alert" className="mt-1 text-warning-foreground">No API key for {quote.missing_credentials.join(", ")}: those steps are skipped. Add the keys in {brand.linkLabel} settings.</p> : null}
|
|
163
209
|
{quote.warnings.map((w) => <p key={w} className="mt-1 text-xs text-muted-foreground">{w}</p>)}
|
|
164
210
|
</div>
|
|
165
211
|
) : null}
|
|
166
212
|
{start.error ? <ApiError error={start.error} title="Could not start the enrichment" /> : null}
|
|
167
213
|
|
|
168
214
|
<div className="flex justify-end">
|
|
169
|
-
<Button disabled={ids.length === 0 ||
|
|
215
|
+
<Button disabled={ids.length === 0 || runProviders.length === 0 || preflight.loading || runnable === 0 || Boolean(preflight.error)} onClick={() => setConfirmOpen(true)}>
|
|
170
216
|
Enrich {plural(runnable, "contact")}
|
|
171
217
|
</Button>
|
|
172
218
|
</div>
|
|
@@ -175,7 +221,7 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
175
221
|
open={confirmOpen}
|
|
176
222
|
tier="billable"
|
|
177
223
|
title={`Enrich ${runnable.toLocaleString()} contacts: ${column.label.toLowerCase()}`}
|
|
178
|
-
description={`Providers in order: ${
|
|
224
|
+
description={`Providers in order: ${runProviders.join(", ")}${existing ? ` (the list's saved ${column.label.toLowerCase()} pipeline)` : ""}. The estimate is the most this run can charge; your balance is not checked here.`}
|
|
179
225
|
estimatedCredits={credits}
|
|
180
226
|
affected={runnable}
|
|
181
227
|
confirmLabel="Start enrichment"
|
|
@@ -194,15 +240,18 @@ function BulkEnrichCore({ listId, contactIds, defaultProviders = DEFAULT_PROVIDE
|
|
|
194
240
|
export function BulkEnrichPanel(props: BulkEnrichPanelProps) {
|
|
195
241
|
const [listId, setListId] = useState<number | undefined>(undefined);
|
|
196
242
|
const [picked, setPicked] = useState<number[]>([]);
|
|
243
|
+
const [tableVersion, setTableVersion] = useState(0);
|
|
197
244
|
if (props.listId != null && props.contactIds) return <BulkEnrichCore {...props} listId={props.listId} contactIds={props.contactIds} />;
|
|
198
245
|
const { listId: _l, contactIds: _c, ...rest } = props;
|
|
246
|
+
// A finished run reloads the table so the filled values show (this also clears the ticked contacts).
|
|
247
|
+
const done = (job: EnrichJob) => { setTableVersion((v) => v + 1); props.onDone?.(job); };
|
|
199
248
|
return (
|
|
200
249
|
<div className={`flex flex-col gap-4 ${props.className ?? ""}`}>
|
|
201
250
|
<ListPicker value={listId} allowCreate={false} onChange={(l) => { setListId(l.id); setPicked([]); }} />
|
|
202
251
|
{listId != null ? (
|
|
203
252
|
<>
|
|
204
|
-
<ContactTable listId={listId} selectable pageSize={10} onSelectionChange={(ids) => setPicked(ids)} />
|
|
205
|
-
<BulkEnrichCore {...rest} className={undefined} listId={listId} contactIds={picked} />
|
|
253
|
+
<ContactTable listId={listId} selectable pageSize={10} refreshKey={tableVersion} onSelectionChange={(ids) => setPicked(ids)} />
|
|
254
|
+
<BulkEnrichCore {...rest} className={undefined} listId={listId} contactIds={picked} onDone={done} />
|
|
206
255
|
</>
|
|
207
256
|
) : <p className="text-sm text-muted-foreground">Choose a list to enrich its contacts.</p>}
|
|
208
257
|
</div>
|
|
@@ -63,9 +63,9 @@ export function classifyVerification(status: string | null | undefined, isValid?
|
|
|
63
63
|
|
|
64
64
|
const STYLE: Record<VerifyState, string> = {
|
|
65
65
|
unverified: "bg-muted text-muted-foreground",
|
|
66
|
-
valid: "bg-
|
|
67
|
-
risky: "bg-
|
|
68
|
-
invalid: "bg-
|
|
66
|
+
valid: "bg-positive text-positive-foreground",
|
|
67
|
+
risky: "bg-warning text-warning-foreground",
|
|
68
|
+
invalid: "bg-error text-error-foreground",
|
|
69
69
|
unverifiable: "bg-muted text-muted-foreground",
|
|
70
70
|
};
|
|
71
71
|
const LABEL: Record<VerifyState, string> = { unverified: "Unverified", valid: "Valid", risky: "Risky", invalid: "Invalid", unverifiable: "Couldn't verify" };
|
|
@@ -138,7 +138,7 @@ export function EnrichButton({ kind, lookup, record, recordLoaded = false, label
|
|
|
138
138
|
<li key={k} className="flex items-center gap-2">
|
|
139
139
|
{target ? <input type="checkbox" aria-label={`Apply ${k}`} checked={ticked.has(k)} onChange={() => setTicked((t) => { const n = new Set(t); if (!n.delete(k)) n.add(k); return n; })} /> : null}
|
|
140
140
|
<span className="w-36 text-muted-foreground">{k.replace(/_/g, " ")}</span><span className="truncate">{v}</span>
|
|
141
|
-
{target?.checked && target.existing[k] && !isMaskedValue(target.existing[k]) && target.existing[k] !== v ? <span className="text-xs text-
|
|
141
|
+
{target?.checked && target.existing[k] && !isMaskedValue(target.existing[k]) && target.existing[k] !== v ? <span className="text-xs text-warning-foreground">replaces {String(target.existing[k])}</span> : null}
|
|
142
142
|
</li>
|
|
143
143
|
))}
|
|
144
144
|
{Object.keys(fields).length === 0 ? <li className="text-muted-foreground">The match had no fields to show.</li> : null}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Provider keys are added in graph8 settings (no API), so the Providers tab links out for them.
|
|
15
15
|
*
|
|
16
16
|
* SDK operations (inside the parts it composes): listEnrichmentProviders, preflightEnrichment (read),
|
|
17
|
-
*
|
|
17
|
+
* createDefaultWaterfallConfig + runWaterfallPipeline (billable, ConfirmSpendDialog), getEnrichmentJob, cancelEnrichmentJob, lookupPerson / lookupCompany
|
|
18
18
|
* (billable, confirmed), verifyEmail (billable, confirmed), listWaterfallConfigs, createDefaultWaterfallConfig (billable,
|
|
19
19
|
* confirmed), deleteWaterfallConfig (destructive, confirmed), listContacts, getContact.
|
|
20
20
|
*/
|
|
@@ -31,7 +31,13 @@ import { SplitView } from "@/g8/components/screen/split-view";
|
|
|
31
31
|
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
32
32
|
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
33
33
|
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
34
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
34
35
|
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
36
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
37
|
+
import { CompanyPicker } from "@/g8/pickers/components/company-picker";
|
|
38
|
+
import type { CompanyOption } from "@/g8/pickers/components/company-picker";
|
|
39
|
+
import { ContactPicker } from "@/g8/pickers/components/contact-picker";
|
|
40
|
+
import type { ContactOption } from "@/g8/pickers/components/contact-picker";
|
|
35
41
|
import { ContactDetail } from "@/g8/crm/components/contact-detail";
|
|
36
42
|
import { ContactTable } from "@/g8/crm/components/contact-table";
|
|
37
43
|
import type { ContactRecord } from "@/g8/crm/lib/crm-types";
|
|
@@ -75,7 +81,7 @@ function PipelineDetail({ config, onClose, onExpand, onCollapse }: { config: Wat
|
|
|
75
81
|
<DetailPanel onExpand={onExpand} onCollapse={onCollapse} title={config.name} subtitle={`Fills ${config.field_to_enrich ?? "a field"} in ${plural(config.step_count, "step")}`} onClose={onClose}
|
|
76
82
|
badges={config.is_active === false ? <StatusPill tone="neutral" dot>Inactive</StatusPill> : <StatusPill tone="positive" dot>Active</StatusPill>}>
|
|
77
83
|
<div className="flex flex-col gap-4 p-1">
|
|
78
|
-
{config.missing_credentials.length ? <p role="alert" className="text-sm text-
|
|
84
|
+
{config.missing_credentials.length ? <p role="alert" className="text-sm text-warning-foreground">No key for {config.missing_credentials.join(", ")}: those steps are skipped when it runs. Add the keys in {brand.linkLabel} settings.</p> : null}
|
|
79
85
|
<ol className="flex flex-col gap-2" aria-label="Pipeline steps">
|
|
80
86
|
{(config.providers_detail.length ? config.providers_detail : config.providers.map((p) => ({ provider: p, is_system_provider: false, credential_source: "system" as const, credential_ready: true }))).map((p, i) => (
|
|
81
87
|
<li key={`${p.provider}-${i}`} className="flex items-center gap-2 text-sm">
|
|
@@ -94,18 +100,42 @@ function PipelineDetail({ config, onClose, onExpand, onCollapse }: { config: Wat
|
|
|
94
100
|
);
|
|
95
101
|
}
|
|
96
102
|
|
|
103
|
+
interface ActiveJob { job_id: string; status?: string | null; column_id?: string | null; enrichment_type?: string | null; completed?: number | null; total?: number | null; failed?: number | null; paused_at?: string | null }
|
|
104
|
+
|
|
105
|
+
/** Jobs running, paused or pending in the last six hours, org-wide (GET /enrichment/active-jobs, read, free): no job id to remember. */
|
|
106
|
+
function ActiveJobs({ jobParam, onOpen }: { jobParam: string; onOpen: (id: string) => void }) {
|
|
107
|
+
const q = useG8Query(async (signal) => {
|
|
108
|
+
const d = (await g8Get("/enrichment/active-jobs", undefined, { signal })).data as { jobs?: ActiveJob[] } | ActiveJob[] | undefined;
|
|
109
|
+
return (Array.isArray(d) ? d : (d?.jobs ?? [])).filter((j) => j?.job_id);
|
|
110
|
+
}, []);
|
|
111
|
+
const rows = q.data ?? [];
|
|
112
|
+
return (
|
|
113
|
+
<SectionCard title="Running now" description="Jobs from the last 6 hours, from anyone on your team." flush actions={<Button size="sm" variant="ghost" onClick={() => void q.refetch()}>Refresh</Button>}>
|
|
114
|
+
{q.error ? <div className="p-3"><ApiError error={q.error} title="Could not load running jobs" onRetry={() => void q.refetch()} /></div> : null}
|
|
115
|
+
{!q.error && q.loading && rows.length === 0 ? <p className="p-4 text-sm text-muted-foreground">Loading jobs...</p> : null}
|
|
116
|
+
{!q.error && !q.loading && rows.length === 0 ? <EmptyState title="Nothing running" description="Jobs that are running, paused or pending show here." /> : null}
|
|
117
|
+
<ul className="divide-y">{rows.map((j) => (
|
|
118
|
+
<li key={j.job_id}>
|
|
119
|
+
<button type="button" aria-label={`Open job ${j.column_id ?? j.enrichment_type ?? j.job_id}`} className={`flex w-full items-center justify-between gap-2 px-4 py-2 text-left text-sm ${j.job_id === jobParam ? "bg-muted/50" : ""}`} onClick={() => onOpen(j.job_id)}>
|
|
120
|
+
<span className="truncate">{j.column_id ?? j.enrichment_type ?? "Enrichment"}</span>
|
|
121
|
+
<span className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
122
|
+
<span className="tabular-nums">{j.completed ?? 0}/{j.total ?? 0}</span>
|
|
123
|
+
<StatusPill tone={j.status === "running" ? "positive" : "neutral"} dot>{j.status ?? "unknown"}</StatusPill>
|
|
124
|
+
</span>
|
|
125
|
+
</button>
|
|
126
|
+
</li>
|
|
127
|
+
))}</ul>
|
|
128
|
+
</SectionCard>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
97
132
|
function JobsTab({ jobs, jobParam, setJobParam }: { jobs: string[]; jobParam: string; setJobParam: (v: string | null) => void }) {
|
|
98
133
|
const [typed, setTyped] = useState("");
|
|
99
134
|
const [view, setView] = useUrlState("view");
|
|
100
135
|
const full = view === "full";
|
|
101
136
|
const list = (
|
|
102
137
|
<div className="flex flex-col gap-3 p-1">
|
|
103
|
-
<
|
|
104
|
-
<form className="flex gap-2" onSubmit={(e) => { e.preventDefault(); if (typed.trim()) setJobParam(typed.trim()); }}>
|
|
105
|
-
<Input aria-label="Job id" value={typed} onChange={(e) => setTyped(e.target.value)} placeholder="Job id" />
|
|
106
|
-
<Button type="submit" size="sm" variant="outline" disabled={!typed.trim()}>Open</Button>
|
|
107
|
-
</form>
|
|
108
|
-
</SectionCard>
|
|
138
|
+
<ActiveJobs jobParam={jobParam} onOpen={setJobParam} />
|
|
109
139
|
<SectionCard title="Started in this session" flush>
|
|
110
140
|
{jobs.length === 0 ? <EmptyState title="No jobs yet" description="Enrich contacts and the job appears here, with live progress." /> : (
|
|
111
141
|
<ul className="divide-y">{jobs.map((id) => (
|
|
@@ -113,6 +143,13 @@ function JobsTab({ jobs, jobParam, setJobParam }: { jobs: string[]; jobParam: st
|
|
|
113
143
|
))}</ul>
|
|
114
144
|
)}
|
|
115
145
|
</SectionCard>
|
|
146
|
+
<details className="px-1 text-sm">
|
|
147
|
+
<summary className="cursor-pointer text-muted-foreground">Open an older job by id</summary>
|
|
148
|
+
<form className="mt-2 flex gap-2" onSubmit={(e) => { e.preventDefault(); if (typed.trim()) setJobParam(typed.trim()); }}>
|
|
149
|
+
<Input aria-label="Job id" value={typed} onChange={(e) => setTyped(e.target.value)} placeholder="Job id" />
|
|
150
|
+
<Button type="submit" size="sm" variant="outline" disabled={!typed.trim()}>Open</Button>
|
|
151
|
+
</form>
|
|
152
|
+
</details>
|
|
116
153
|
</div>
|
|
117
154
|
);
|
|
118
155
|
return (
|
|
@@ -138,8 +175,8 @@ function LookupTab() {
|
|
|
138
175
|
const [cDomain, setCDomain] = useState("");
|
|
139
176
|
const [cName, setCName] = useState("");
|
|
140
177
|
const [verifyEmail, setVerifyEmail] = useState("");
|
|
141
|
-
const [
|
|
142
|
-
const [
|
|
178
|
+
const [contactPick, setContactPick] = useState<ContactOption | null>(null);
|
|
179
|
+
const [companyPick, setCompanyPick] = useState<CompanyOption | null>(null);
|
|
143
180
|
const [applied, setApplied] = useState<string | null>(null);
|
|
144
181
|
const person = { email: email.trim() || undefined, linkedin_url: linkedin.trim() || undefined, first_name: first.trim() || undefined, last_name: last.trim() || undefined, company_domain: domain.trim() || undefined };
|
|
145
182
|
const company = { domain: cDomain.trim() || undefined, name: cName.trim() || undefined };
|
|
@@ -155,9 +192,15 @@ function LookupTab() {
|
|
|
155
192
|
</div>
|
|
156
193
|
<div className={FIELD}><Label htmlFor="lk-domain">Company domain</Label><Input id="lk-domain" placeholder="acme.com" value={domain} onChange={(e) => setDomain(e.target.value)} /></div>
|
|
157
194
|
<p className="text-xs text-muted-foreground">Use an email, a LinkedIn URL, or first name, last name and company domain together.</p>
|
|
158
|
-
<div className={FIELD}
|
|
195
|
+
<div className={FIELD}>
|
|
196
|
+
<Label htmlFor="lk-rec">Save the result on a contact (optional)</Label>
|
|
197
|
+
<div className="flex items-center gap-2">
|
|
198
|
+
<ContactPicker id="lk-rec" label="Contact to update" placeholder="Choose a contact" value={contactPick} onChange={setContactPick} />
|
|
199
|
+
{contactPick ? <Button size="sm" variant="ghost" onClick={() => setContactPick(null)}>Clear</Button> : null}
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
159
202
|
<div className="flex items-center gap-2">
|
|
160
|
-
<EnrichButton kind="person" lookup={person} record={
|
|
203
|
+
<EnrichButton kind="person" lookup={person} record={contactPick ? { id: contactPick.id } : undefined} label="Find person" variant="default" size="default" onApplied={() => setApplied(`Updated ${contactPick?.name ?? "the contact"}.`)} />
|
|
161
204
|
{applied ? <span role="status" className="text-sm">{applied}</span> : null}
|
|
162
205
|
</div>
|
|
163
206
|
</div>
|
|
@@ -167,8 +210,14 @@ function LookupTab() {
|
|
|
167
210
|
<div className="flex flex-col gap-3">
|
|
168
211
|
<div className={FIELD}><Label htmlFor="lk-cdomain">Domain</Label><Input id="lk-cdomain" placeholder="acme.com" value={cDomain} onChange={(e) => setCDomain(e.target.value)} /></div>
|
|
169
212
|
<div className={FIELD}><Label htmlFor="lk-cname">Name</Label><Input id="lk-cname" value={cName} onChange={(e) => setCName(e.target.value)} /></div>
|
|
170
|
-
<div className={FIELD}
|
|
171
|
-
|
|
213
|
+
<div className={FIELD}>
|
|
214
|
+
<Label htmlFor="lk-crec">Save the result on a company (optional)</Label>
|
|
215
|
+
<div className="flex items-center gap-2">
|
|
216
|
+
<CompanyPicker id="lk-crec" label="Company to update" placeholder="Choose a company" value={companyPick} onChange={setCompanyPick} />
|
|
217
|
+
{companyPick ? <Button size="sm" variant="ghost" onClick={() => setCompanyPick(null)}>Clear</Button> : null}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
<div><EnrichButton kind="company" lookup={company} record={companyPick ? { id: companyPick.id } : undefined} label="Find company" variant="default" size="default" /></div>
|
|
172
221
|
</div>
|
|
173
222
|
</SectionCard>
|
|
174
223
|
<SectionCard title="Verify an email" description={`${CREDITS_PER_EMAIL_VERIFICATION} credit per check.`}>
|
|
@@ -210,6 +259,10 @@ export function EnrichmentScreen({ toolbarActions, renderDetailActions, extraDet
|
|
|
210
259
|
const [tab, setTab] = useUrlState("tab", "list");
|
|
211
260
|
const [listParam, setListParam] = useUrlState("list");
|
|
212
261
|
const [picked, setPicked] = useState<number[]>([]);
|
|
262
|
+
// A finished run reloads the contact table (tableVersion) so the filled values show; the reload clears the ticks,
|
|
263
|
+
// so `runShown` keeps the panel and its result on screen until the person ticks contacts again.
|
|
264
|
+
const [tableVersion, setTableVersion] = useState(0);
|
|
265
|
+
const [runShown, setRunShown] = useState(false);
|
|
213
266
|
const [opened, setOpened] = useState<ContactRecord | null>(null);
|
|
214
267
|
const catalog = useProviders(tab === "providers");
|
|
215
268
|
const [jobParam, setJobParam] = useUrlState("job");
|
|
@@ -220,7 +273,7 @@ export function EnrichmentScreen({ toolbarActions, renderDetailActions, extraDet
|
|
|
220
273
|
useEffect(() => setJobs(loadJobs()), []);
|
|
221
274
|
const started = (id: string) => setJobs((cur) => { const next = [id, ...cur.filter((x) => x !== id)]; rememberJobs(next); return next; });
|
|
222
275
|
const listId = listParam ? Number(listParam) : null;
|
|
223
|
-
const pick = (id: number) => { setListParam(String(id)); setPicked([]); setOpened(null); setPipelineParam(null); };
|
|
276
|
+
const pick = (id: number) => { setListParam(String(id)); setPicked([]); setRunShown(false); setOpened(null); setPipelineParam(null); };
|
|
224
277
|
|
|
225
278
|
const stats = tab === "providers" && catalog.providers.length > 0 ? [
|
|
226
279
|
{ label: "Providers", value: catalog.providers.length },
|
|
@@ -248,10 +301,10 @@ export function EnrichmentScreen({ toolbarActions, renderDetailActions, extraDet
|
|
|
248
301
|
detail={openPipeline ? <PipelineDetail config={openPipeline} onClose={() => setPipelineParam(null)} onExpand={full ? undefined : () => setView("full")} onCollapse={full ? () => setView(null) : undefined} /> : null} />
|
|
249
302
|
) : (
|
|
250
303
|
<div className="grid min-w-0 gap-4 xl:grid-cols-2">
|
|
251
|
-
<ContactTable key={listId} listId={listId} selectable pageSize={10} onSelectionChange={(ids) => setPicked(ids)} onOpen={setOpened} />
|
|
304
|
+
<ContactTable key={listId} listId={listId} selectable pageSize={10} refreshKey={tableVersion} onSelectionChange={(ids) => setPicked(ids)} onOpen={setOpened} />
|
|
252
305
|
<div className="flex min-w-0 flex-col gap-4">
|
|
253
306
|
<SectionCard title={`Enrich ${picked.length} selected`} description="Fill work email or phones with a provider waterfall. You confirm the price first.">
|
|
254
|
-
{picked.length === 0 ? <p className="text-sm text-muted-foreground">Tick contacts on the left.</p> : <BulkEnrichPanel key={listId} listId={listId} contactIds={picked} onStarted={started} />}
|
|
307
|
+
{picked.length === 0 && !runShown ? <p className="text-sm text-muted-foreground">Tick contacts on the left.</p> : <BulkEnrichPanel key={listId} listId={listId} contactIds={picked} onStarted={(id) => { setRunShown(true); started(id); }} onDone={() => setTableVersion((v) => v + 1)} />}
|
|
255
308
|
</SectionCard>
|
|
256
309
|
{opened ? (
|
|
257
310
|
<DetailPanel key={opened.id} title={opened.full_name ?? ([opened.first_name, opened.last_name].filter(Boolean).join(" ") || `Contact #${opened.id}`)} subtitle={opened.job_title} onClose={() => setOpened(null)}>
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
* providers, whatever name and providers were sent. The form then says so instead of claiming a new pipeline.
|
|
14
14
|
* 4. deleteWaterfallConfig DELETE /enrichment/waterfall/configs/{column_id} (destructive, ConfirmSpendDialog)
|
|
15
15
|
*
|
|
16
|
+
* The provider choices follow the chosen field (the API refuses a provider that cannot fill it with 400
|
|
17
|
+
* `unsupported_provider_for_field`): changing the field drops selected providers that no longer fit and says which.
|
|
18
|
+
*
|
|
16
19
|
* Providers on graph8's system key spend credits when the pipeline RUNS; providers on the org's own key (BYOK) do not.
|
|
17
20
|
* A BYOK provider without a key is listed under `missing_credentials` and is skipped at run time.
|
|
18
21
|
*/
|
|
@@ -27,7 +30,7 @@ import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
|
27
30
|
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
28
31
|
import { plural } from "@/g8/lib/plural";
|
|
29
32
|
import { useProviders } from "../hooks/use-providers";
|
|
30
|
-
import { TARGET_FIELDS } from "../lib/enrichment-types";
|
|
33
|
+
import { FIELD_PROVIDERS, TARGET_FIELDS } from "../lib/enrichment-types";
|
|
31
34
|
import type { TargetField, WaterfallConfigSummary } from "../lib/enrichment-types";
|
|
32
35
|
import { useBrand } from "@/g8/hooks/use-brand";
|
|
33
36
|
|
|
@@ -54,6 +57,8 @@ export function WaterfallConfig({ listId, onChanged, onOpen, selectedId, onLoade
|
|
|
54
57
|
const [deleting, setDeleting] = useState<WaterfallConfigSummary | null>(null);
|
|
55
58
|
// The answer to the last create when the API reused the list's pipeline for that field instead of creating one.
|
|
56
59
|
const [reused, setReused] = useState<{ name?: string; field: TargetField; providers: string[]; asked: string[] } | null>(null);
|
|
60
|
+
// Providers dropped by the last field change, with the field they cannot fill, so the person sees why they went.
|
|
61
|
+
const [dropped, setDropped] = useState<{ providers: string[]; field: TargetField } | null>(null);
|
|
57
62
|
const catalog = useProviders();
|
|
58
63
|
|
|
59
64
|
const configs = useG8Query((signal) => g8Get<{ configs: WaterfallConfigSummary[] }>("/enrichment/waterfall/configs", { list_id: listId }, { signal }), [listId]);
|
|
@@ -76,6 +81,16 @@ export function WaterfallConfig({ listId, onChanged, onOpen, selectedId, onLoade
|
|
|
76
81
|
const loadedRef = useRef(onLoaded);
|
|
77
82
|
loadedRef.current = onLoaded;
|
|
78
83
|
useEffect(() => { if (configs.data) loadedRef.current?.(configs.data.data.configs ?? []); }, [configs.data]);
|
|
84
|
+
const fieldLabel = TARGET_FIELDS.find((f) => f.value === field)?.label ?? field;
|
|
85
|
+
const allowed = FIELD_PROVIDERS[field];
|
|
86
|
+
const unfit = order.filter((p) => !allowed.includes(p));
|
|
87
|
+
const changeField = (next: TargetField) => {
|
|
88
|
+
const gone = order.filter((p) => !FIELD_PROVIDERS[next].includes(p));
|
|
89
|
+
setField(next);
|
|
90
|
+
setOrder(order.filter((p) => FIELD_PROVIDERS[next].includes(p)));
|
|
91
|
+
setDropped(gone.length ? { providers: gone, field: next } : null);
|
|
92
|
+
setReused(null);
|
|
93
|
+
};
|
|
79
94
|
const byName = new Map(catalog.providers.map((p) => [p.provider, p]));
|
|
80
95
|
const systemUsed = order.filter((p) => byName.get(p)?.system_funded && !byName.get(p)?.byok_configured);
|
|
81
96
|
const move = (p: string, d: number) => {
|
|
@@ -102,7 +117,7 @@ export function WaterfallConfig({ listId, onChanged, onOpen, selectedId, onLoade
|
|
|
102
117
|
<Button variant="ghost" size="sm" onClick={() => setDeleting(c)}>Delete</Button>
|
|
103
118
|
</div>
|
|
104
119
|
<p className="text-xs text-muted-foreground">{c.field_to_enrich ?? "field"}: {c.providers.join(" > ") || "no providers"} ({plural(c.step_count, "step")})</p>
|
|
105
|
-
{c.missing_credentials.length ? <p role="alert" className="mt-1 text-xs text-
|
|
120
|
+
{c.missing_credentials.length ? <p role="alert" className="mt-1 text-xs text-warning-foreground">Skipped at run time, no key: {c.missing_credentials.join(", ")}</p> : null}
|
|
106
121
|
</li>
|
|
107
122
|
))}
|
|
108
123
|
</ul>
|
|
@@ -113,7 +128,7 @@ export function WaterfallConfig({ listId, onChanged, onOpen, selectedId, onLoade
|
|
|
113
128
|
<div className="flex flex-wrap items-end gap-3">
|
|
114
129
|
<div className="flex flex-col gap-1"><Label htmlFor="wf-name">Name</Label><Input id="wf-name" className="w-64" value={name} onChange={(e) => setName(e.target.value)} /></div>
|
|
115
130
|
<div className="flex flex-col gap-1"><Label htmlFor="wf-field">Fill</Label>
|
|
116
|
-
<select id="wf-field" className="h-9 rounded-md border bg-background px-2 text-sm" value={field} onChange={(e) =>
|
|
131
|
+
<select id="wf-field" className="h-9 rounded-md border bg-background px-2 text-sm" value={field} onChange={(e) => changeField(e.target.value as TargetField)}>
|
|
117
132
|
{TARGET_FIELDS.map((f) => <option key={f.value} value={f.value}>{f.label}</option>)}
|
|
118
133
|
</select>
|
|
119
134
|
</div>
|
|
@@ -130,9 +145,14 @@ export function WaterfallConfig({ listId, onChanged, onOpen, selectedId, onLoade
|
|
|
130
145
|
</li>
|
|
131
146
|
))}
|
|
132
147
|
</ol>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
148
|
+
{dropped ? (
|
|
149
|
+
<p role="status" className="text-xs text-muted-foreground">
|
|
150
|
+
Removed {dropped.providers.join(", ")}: {dropped.providers.length === 1 ? "it cannot" : "they cannot"} find a {TARGET_FIELDS.find((f) => f.value === dropped.field)?.label.toLowerCase()}.
|
|
151
|
+
</p>
|
|
152
|
+
) : null}
|
|
153
|
+
<div role="group" className="flex flex-wrap gap-1" aria-label="Provider choices" title={`Providers that can find a ${fieldLabel.toLowerCase()}`}>
|
|
154
|
+
{catalog.providers.filter((p) => allowed.includes(p.provider) && !order.includes(p.provider)).map((p) => (
|
|
155
|
+
<Button key={p.provider} variant="outline" size="sm" onClick={() => { setDropped(null); setOrder([...order, p.provider]); }}>Add {p.provider}</Button>
|
|
136
156
|
))}
|
|
137
157
|
</div>
|
|
138
158
|
<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={skipExisting} onChange={(e) => setSkipExisting(e.target.checked)} /> Skip contacts that already have a value</label>
|
|
@@ -143,7 +163,7 @@ export function WaterfallConfig({ listId, onChanged, onOpen, selectedId, onLoade
|
|
|
143
163
|
{reused.providers.length && reused.providers.join() !== reused.asked.join() ? ` It keeps its providers: ${reused.providers.join(" > ")}. Delete it first to use ${reused.asked.join(" > ")}.` : ""}
|
|
144
164
|
</p>
|
|
145
165
|
) : null}
|
|
146
|
-
<div className="flex justify-end"><Button disabled={!name.trim() || order.length === 0} onClick={() => setConfirmCreate(true)}>Create pipeline</Button></div>
|
|
166
|
+
<div className="flex justify-end"><Button disabled={!name.trim() || order.length === 0 || unfit.length > 0} onClick={() => setConfirmCreate(true)}>Create pipeline</Button></div>
|
|
147
167
|
</section>
|
|
148
168
|
|
|
149
169
|
<ConfirmSpendDialog open={confirmCreate} tier="billable" title={`Create pipeline "${name.trim()}"`}
|