@graph8/sdk 0.303.0 → 0.357.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 +81 -0
- package/blocks/README.md +32 -0
- package/blocks/_foundation/README.md +78 -0
- package/blocks/_foundation/block.json +79 -0
- package/blocks/_foundation/components/api-error.tsx +62 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
- package/blocks/_foundation/components/empty-state.tsx +24 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
- package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
- package/blocks/_foundation/hooks/use-paginated.ts +105 -0
- package/blocks/_foundation/lib/g8-client.ts +128 -0
- package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
- package/blocks/_foundation/server/proxy.ts +233 -0
- package/blocks/_foundation/server/route.ts +30 -0
- package/blocks/agents/README.md +133 -0
- package/blocks/agents/block.json +223 -0
- package/blocks/agents/components/agent-builder.tsx +199 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
- package/blocks/agents/components/agent-runs.tsx +175 -0
- package/blocks/agents/components/agent-skills-panel.tsx +126 -0
- package/blocks/agents/components/agent-test-console.tsx +175 -0
- package/blocks/agents/lib/agent-types.ts +114 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
- package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
- package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
- package/blocks/analytics/README.md +73 -0
- package/blocks/analytics/block.json +178 -0
- package/blocks/analytics/components/funnel-chart.tsx +79 -0
- package/blocks/analytics/components/kpi-tiles.tsx +74 -0
- package/blocks/analytics/components/leaderboard.tsx +86 -0
- package/blocks/analytics/components/report-builder.tsx +200 -0
- package/blocks/analytics/hooks/use-report.ts +26 -0
- package/blocks/analytics/lib/analytics-types.ts +108 -0
- package/blocks/analytics/lib/use-debounced.ts +16 -0
- package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
- package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
- package/blocks/analytics/recipes/03-saved-report.ts +30 -0
- package/blocks/appointments/README.md +80 -0
- package/blocks/appointments/block.json +218 -0
- package/blocks/appointments/components/availability-editor.tsx +192 -0
- package/blocks/appointments/components/booking-page.tsx +154 -0
- package/blocks/appointments/components/event-type-editor.tsx +214 -0
- package/blocks/appointments/components/meeting-detail.tsx +147 -0
- package/blocks/appointments/components/meeting-list.tsx +148 -0
- package/blocks/appointments/hooks/use-available-slots.ts +29 -0
- package/blocks/appointments/hooks/use-schedules.ts +18 -0
- package/blocks/appointments/lib/appointments-types.ts +159 -0
- package/blocks/appointments/lib/idempotency.ts +30 -0
- package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
- package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
- package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
- package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
- package/blocks/block.schema.json +130 -0
- package/blocks/block.types.ts +109 -0
- package/blocks/campaigns/README.md +118 -0
- package/blocks/campaigns/block.json +361 -0
- package/blocks/campaigns/components/campaign-assets.tsx +180 -0
- package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
- package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
- package/blocks/campaigns/components/campaign-list.tsx +120 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
- package/blocks/campaigns/components/campaign-select.tsx +52 -0
- package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
- package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
- package/blocks/campaigns/components/form-field.tsx +29 -0
- package/blocks/campaigns/components/icp-editor.tsx +230 -0
- package/blocks/campaigns/components/persona-editor.tsx +210 -0
- package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
- package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
- package/blocks/campaigns/recipes/campaign-report.ts +22 -0
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
- package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
- package/blocks/coach/README.md +93 -0
- package/blocks/coach/block.json +125 -0
- package/blocks/coach/components/call-scorecard.tsx +126 -0
- package/blocks/coach/components/coaching-feed.tsx +140 -0
- package/blocks/coach/lib/coach-types.ts +116 -0
- package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
- package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
- package/blocks/copilot/README.md +99 -0
- package/blocks/copilot/block.json +151 -0
- package/blocks/copilot/components/copilot-memory.tsx +87 -0
- package/blocks/copilot/components/copilot-panel.tsx +189 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
- package/blocks/copilot/lib/copilot-types.ts +111 -0
- package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
- package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
- package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
- package/blocks/crm/README.md +122 -0
- package/blocks/crm/block.json +378 -0
- package/blocks/crm/components/activity-timeline.tsx +99 -0
- package/blocks/crm/components/company-detail.tsx +118 -0
- package/blocks/crm/components/company-table.tsx +173 -0
- package/blocks/crm/components/contact-detail.tsx +135 -0
- package/blocks/crm/components/contact-table.tsx +217 -0
- package/blocks/crm/components/dedupe-review.tsx +164 -0
- package/blocks/crm/components/import-wizard.tsx +193 -0
- package/blocks/crm/components/notes-panel.tsx +112 -0
- package/blocks/crm/components/record-form.tsx +191 -0
- package/blocks/crm/components/task-list.tsx +107 -0
- package/blocks/crm/hooks/use-custom-fields.ts +18 -0
- package/blocks/crm/hooks/use-debounced.ts +13 -0
- package/blocks/crm/hooks/use-page-query.ts +52 -0
- package/blocks/crm/lib/crm-types.ts +136 -0
- package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
- package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
- package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
- package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
- package/blocks/deals/README.md +78 -0
- package/blocks/deals/block.json +198 -0
- package/blocks/deals/components/deal-detail.tsx +134 -0
- package/blocks/deals/components/forecast-view.tsx +112 -0
- package/blocks/deals/components/pipeline-board.tsx +142 -0
- package/blocks/deals/components/stage-editor.tsx +96 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
- package/blocks/deals/hooks/use-pipelines.ts +19 -0
- package/blocks/deals/lib/deals-types.ts +128 -0
- package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
- package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
- package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
- package/blocks/dialer/README.md +114 -0
- package/blocks/dialer/block.json +260 -0
- package/blocks/dialer/components/call-log.tsx +78 -0
- package/blocks/dialer/components/call-queue.tsx +188 -0
- package/blocks/dialer/components/dialer-workspace.tsx +47 -0
- package/blocks/dialer/components/disposition-panel.tsx +75 -0
- package/blocks/dialer/components/live-calls-table.tsx +107 -0
- package/blocks/dialer/components/session-start-form.tsx +111 -0
- package/blocks/dialer/components/softphone.tsx +122 -0
- package/blocks/dialer/components/transcript-viewer.tsx +82 -0
- package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
- package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
- package/blocks/dialer/hooks/use-dispositions.ts +29 -0
- package/blocks/dialer/lib/dialer-types.ts +158 -0
- package/blocks/dialer/lib/softphone-transport.ts +67 -0
- package/blocks/dialer/lib/twilio-transport.ts +74 -0
- package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
- package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
- package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
- package/blocks/enrichment/README.md +105 -0
- package/blocks/enrichment/block.json +204 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
- package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
- package/blocks/enrichment/components/enrich-button.tsx +136 -0
- package/blocks/enrichment/components/waterfall-config.tsx +127 -0
- package/blocks/enrichment/hooks/use-providers.ts +18 -0
- package/blocks/enrichment/lib/enrichment-types.ts +104 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
- package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
- package/blocks/forms/README.md +112 -0
- package/blocks/forms/block.json +200 -0
- package/blocks/forms/components/form-builder.tsx +150 -0
- package/blocks/forms/components/form-embed.tsx +103 -0
- package/blocks/forms/components/form-mapping-editor.tsx +125 -0
- package/blocks/forms/components/site-audit-panel.tsx +73 -0
- package/blocks/forms/components/submissions-table.tsx +158 -0
- package/blocks/forms/lib/form-types.ts +127 -0
- package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
- package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
- package/blocks/forms/recipes/route-form-to-list.ts +44 -0
- package/blocks/inbox/README.md +72 -0
- package/blocks/inbox/block.json +194 -0
- package/blocks/inbox/components/inbox-list.tsx +156 -0
- package/blocks/inbox/components/inbox-screen.tsx +49 -0
- package/blocks/inbox/components/reply-composer.tsx +183 -0
- package/blocks/inbox/components/tag-and-assign.tsx +189 -0
- package/blocks/inbox/components/thread-badge.tsx +17 -0
- package/blocks/inbox/components/thread-view.tsx +108 -0
- package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
- package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
- package/blocks/inbox/lib/idempotency.ts +30 -0
- package/blocks/inbox/lib/inbox-types.ts +100 -0
- package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
- package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
- package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
- package/blocks/integrations/README.md +75 -0
- package/blocks/integrations/block.json +172 -0
- package/blocks/integrations/components/integration-list.tsx +165 -0
- package/blocks/integrations/components/sync-status.tsx +223 -0
- package/blocks/integrations/components/webhook-manager.tsx +300 -0
- package/blocks/integrations/lib/integrations-utils.ts +62 -0
- package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
- package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
- package/blocks/integrations/recipes/register-webhook.ts +18 -0
- package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
- package/blocks/intent/README.md +57 -0
- package/blocks/intent/block.json +138 -0
- package/blocks/intent/components/account-intent-score.tsx +91 -0
- package/blocks/intent/components/intent-feed.tsx +215 -0
- package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
- package/blocks/intent/hooks/use-account-intent.ts +46 -0
- package/blocks/intent/lib/intent-utils.ts +95 -0
- package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
- package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
- package/blocks/intent/recipes/who-visited-url.ts +18 -0
- package/blocks/lifecycle/README.md +79 -0
- package/blocks/lifecycle/block.json +198 -0
- package/blocks/lifecycle/components/churn-list.tsx +92 -0
- package/blocks/lifecycle/components/customer-table.tsx +119 -0
- package/blocks/lifecycle/components/lead-queue.tsx +127 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
- package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
- package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
- package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
- package/blocks/lifecycle/lib/lifecycle.ts +99 -0
- package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
- package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
- package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
- package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
- package/blocks/lists/README.md +96 -0
- package/blocks/lists/block.json +166 -0
- package/blocks/lists/components/add-to-list-button.tsx +111 -0
- package/blocks/lists/components/list-picker.tsx +94 -0
- package/blocks/lists/components/list-table.tsx +160 -0
- package/blocks/lists/components/suppression-manager.tsx +153 -0
- package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
- package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
- package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
- package/blocks/mailboxes/README.md +71 -0
- package/blocks/mailboxes/block.json +187 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
- package/blocks/mailboxes/components/warmup-health.tsx +232 -0
- package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
- package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
- package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
- package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
- package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
- package/blocks/newsletter/README.md +122 -0
- package/blocks/newsletter/block.json +224 -0
- package/blocks/newsletter/components/issue-form.tsx +124 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
- package/blocks/newsletter/components/subscriber-table.tsx +173 -0
- package/blocks/newsletter/lib/newsletter-types.ts +97 -0
- package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
- package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
- package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
- package/blocks/nurture/README.md +110 -0
- package/blocks/nurture/block.json +191 -0
- package/blocks/nurture/components/nurture-builder.tsx +114 -0
- package/blocks/nurture/components/nurture-enroll.tsx +107 -0
- package/blocks/nurture/components/nurture-form.tsx +120 -0
- package/blocks/nurture/components/nurture-stats.tsx +81 -0
- package/blocks/nurture/lib/nurture-types.ts +109 -0
- package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
- package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
- package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
- package/blocks/objects/README.md +120 -0
- package/blocks/objects/block.json +243 -0
- package/blocks/objects/components/attribute-field-input.tsx +82 -0
- package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
- package/blocks/objects/components/object-not-found.tsx +34 -0
- package/blocks/objects/components/object-record-detail.tsx +143 -0
- package/blocks/objects/components/object-schema-editor.tsx +197 -0
- package/blocks/objects/components/object-table.tsx +130 -0
- package/blocks/objects/components/record-form-dialog.tsx +112 -0
- package/blocks/objects/components/record-roles-panel.tsx +106 -0
- package/blocks/objects/hooks/use-object-schema.ts +36 -0
- package/blocks/objects/lib/object-types.ts +214 -0
- package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
- package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
- package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
- package/blocks/pages/README.md +104 -0
- package/blocks/pages/block.json +223 -0
- package/blocks/pages/components/brand-kit-editor.tsx +152 -0
- package/blocks/pages/components/content-library.tsx +163 -0
- package/blocks/pages/components/page-editor.tsx +253 -0
- package/blocks/pages/components/page-list.tsx +159 -0
- package/blocks/pages/components/seo-report.tsx +159 -0
- package/blocks/pages/lib/studio.ts +77 -0
- package/blocks/pages/recipes/audit-site-seo.ts +28 -0
- package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
- package/blocks/pages/recipes/edit-page-section.ts +25 -0
- package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
- package/blocks/quotes/README.md +79 -0
- package/blocks/quotes/block.json +183 -0
- package/blocks/quotes/components/product-picker.tsx +80 -0
- package/blocks/quotes/components/quote-builder.tsx +195 -0
- package/blocks/quotes/components/quote-list.tsx +79 -0
- package/blocks/quotes/components/quote-viewer.tsx +135 -0
- package/blocks/quotes/components/signature-status.tsx +77 -0
- package/blocks/quotes/hooks/use-debounced.ts +13 -0
- package/blocks/quotes/lib/quotes.ts +142 -0
- package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
- package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
- package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
- package/blocks/radar/README.md +106 -0
- package/blocks/radar/block.json +189 -0
- package/blocks/radar/components/competitor-battle-card.tsx +53 -0
- package/blocks/radar/components/competitor-tracker.tsx +158 -0
- package/blocks/radar/components/radar-overview.tsx +67 -0
- package/blocks/radar/components/radar-report.tsx +86 -0
- package/blocks/radar/hooks/use-competitors.ts +23 -0
- package/blocks/radar/lib/radar-types.ts +124 -0
- package/blocks/radar/recipes/battle-card.ts +28 -0
- package/blocks/radar/recipes/deep-dive-report.ts +32 -0
- package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
- package/blocks/radar/recipes/track-and-scan.ts +39 -0
- package/blocks/search/README.md +129 -0
- package/blocks/search/block.json +185 -0
- package/blocks/search/components/filter-builder.tsx +149 -0
- package/blocks/search/components/prospect-results.tsx +175 -0
- package/blocks/search/components/save-to-list-dialog.tsx +135 -0
- package/blocks/search/components/saved-searches.tsx +95 -0
- package/blocks/search/hooks/use-filter-options.ts +41 -0
- package/blocks/search/lib/search-types.ts +149 -0
- package/blocks/search/recipes/explore-filter-values.ts +27 -0
- package/blocks/search/recipes/saved-searches.ts +34 -0
- package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
- package/blocks/sequencer/README.md +87 -0
- package/blocks/sequencer/block.json +214 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
- package/blocks/sequencer/components/sequence-builder.tsx +306 -0
- package/blocks/sequencer/components/sequence-list.tsx +200 -0
- package/blocks/sequencer/components/sequence-stats.tsx +256 -0
- package/blocks/sequencer/components/step-editor.tsx +213 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
- package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
- package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
- package/blocks/sequencer/recipes/sequence-report.ts +22 -0
- package/blocks/social-ads/README.md +128 -0
- package/blocks/social-ads/block.json +256 -0
- package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
- package/blocks/social-ads/components/audience-sync.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +121 -0
- package/blocks/social-ads/components/post-composer.tsx +122 -0
- package/blocks/social-ads/components/social-post-list.tsx +116 -0
- package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
- package/blocks/social-ads/lib/social-ads-types.ts +167 -0
- package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
- package/blocks/social-ads/recipes/schedule-post.ts +51 -0
- package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
- package/blocks/visitors/README.md +56 -0
- package/blocks/visitors/block.json +139 -0
- package/blocks/visitors/components/snippet-install.tsx +169 -0
- package/blocks/visitors/components/traffic-chart.tsx +156 -0
- package/blocks/visitors/components/visitor-company-card.tsx +92 -0
- package/blocks/visitors/components/visitor-feed.tsx +157 -0
- package/blocks/visitors/lib/visitors-utils.ts +78 -0
- package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
- package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
- package/blocks/visitors/recipes/traffic-summary.ts +24 -0
- package/blocks/webchat/README.md +87 -0
- package/blocks/webchat/block.json +134 -0
- package/blocks/webchat/components/chat-widget-config.tsx +160 -0
- package/blocks/webchat/components/conversation-list.tsx +74 -0
- package/blocks/webchat/lib/webchat-types.ts +83 -0
- package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
- package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
- package/blocks/workbench/README.md +135 -0
- package/blocks/workbench/block.json +259 -0
- package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
- package/blocks/workbench/components/run-progress.tsx +132 -0
- package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
- package/blocks/workbench/components/workbench-grid.tsx +159 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
- package/blocks/workbench/components/workbench-table-list.tsx +105 -0
- package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
- package/blocks/workbench/lib/waterfall-presets.ts +119 -0
- package/blocks/workbench/lib/workbench-types.ts +191 -0
- package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
- package/blocks/workbench/recipes/import-from-search.ts +42 -0
- package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
- package/blocks/workbench/recipes/waterfall-column.ts +55 -0
- package/blocks/workflows/README.md +100 -0
- package/blocks/workflows/block.json +188 -0
- package/blocks/workflows/components/run-history.tsx +136 -0
- package/blocks/workflows/components/trigger-config.tsx +134 -0
- package/blocks/workflows/components/workflow-editor.tsx +142 -0
- package/blocks/workflows/components/workflow-list.tsx +153 -0
- package/blocks/workflows/hooks/use-workflow.ts +51 -0
- package/blocks/workflows/lib/graph.ts +187 -0
- package/blocks/workflows/recipes/create-workflow.ts +35 -0
- package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
- package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
- package/blocks/workflows/recipes/set-trigger.ts +22 -0
- package/dist/cli.js +5330 -29
- package/dist/index.browser.js +7740 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.mjs +7717 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +2220 -23
- package/dist/index.d.ts +2220 -23
- package/dist/index.js +64 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +64 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +7598 -0
- package/dist/react.browser.js.map +1 -0
- package/dist/react.browser.mjs +7568 -0
- package/dist/react.browser.mjs.map +1 -0
- package/dist/react.d.mts +2082 -20
- package/dist/react.d.ts +2082 -20
- package/dist/react.js +63 -2
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +63 -2
- package/dist/react.mjs.map +1 -1
- package/dist/types/agentSetup.d.ts +6 -6
- package/dist/types/blocks/add.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +46 -0
- package/dist/types/blocks/doctor.d.ts +26 -0
- package/dist/types/blocks/registry.d.ts +134 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/types/crypto-browser.d.ts +2 -0
- package/dist/types/crypto-node.d.ts +6 -0
- package/dist/types/react.d.ts +1 -1
- package/dist/types/version.d.ts +2 -2
- package/dist/types/webhooks.d.ts +2 -2
- package/dist/ui/index.d.mts +54 -0
- package/dist/ui/index.d.ts +54 -0
- package/dist/ui/index.js +105 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +44 -0
- package/dist/ui/index.mjs.map +1 -0
- package/dist/ui/lib/compose-refs.d.mts +19 -0
- package/dist/ui/lib/compose-refs.d.ts +19 -0
- package/dist/ui/lib/compose-refs.js +76 -0
- package/dist/ui/lib/compose-refs.js.map +1 -0
- package/dist/ui/lib/compose-refs.mjs +41 -0
- package/dist/ui/lib/compose-refs.mjs.map +1 -0
- package/dist/ui/lib/use-mobile.d.mts +4 -0
- package/dist/ui/lib/use-mobile.d.ts +4 -0
- package/dist/ui/lib/use-mobile.js +57 -0
- package/dist/ui/lib/use-mobile.js.map +1 -0
- package/dist/ui/lib/use-mobile.mjs +22 -0
- package/dist/ui/lib/use-mobile.mjs.map +1 -0
- package/dist/ui/lib/utils.d.mts +10 -0
- package/dist/ui/lib/utils.d.ts +10 -0
- package/dist/ui/lib/utils.js +33 -0
- package/dist/ui/lib/utils.js.map +1 -0
- package/dist/ui/lib/utils.mjs +9 -0
- package/dist/ui/lib/utils.mjs.map +1 -0
- package/dist/ui/primitives/alert-dialog.d.mts +22 -0
- package/dist/ui/primitives/alert-dialog.d.ts +22 -0
- package/dist/ui/primitives/alert-dialog.js +163 -0
- package/dist/ui/primitives/alert-dialog.js.map +1 -0
- package/dist/ui/primitives/alert-dialog.mjs +119 -0
- package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
- package/dist/ui/primitives/avatar.d.mts +8 -0
- package/dist/ui/primitives/avatar.d.ts +8 -0
- package/dist/ui/primitives/avatar.js +91 -0
- package/dist/ui/primitives/avatar.js.map +1 -0
- package/dist/ui/primitives/avatar.mjs +55 -0
- package/dist/ui/primitives/avatar.mjs.map +1 -0
- package/dist/ui/primitives/breadcrumb.d.mts +13 -0
- package/dist/ui/primitives/breadcrumb.d.ts +13 -0
- package/dist/ui/primitives/breadcrumb.js +127 -0
- package/dist/ui/primitives/breadcrumb.js.map +1 -0
- package/dist/ui/primitives/breadcrumb.mjs +97 -0
- package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
- package/dist/ui/primitives/button.d.mts +15 -0
- package/dist/ui/primitives/button.d.ts +15 -0
- package/dist/ui/primitives/button.js +91 -0
- package/dist/ui/primitives/button.js.map +1 -0
- package/dist/ui/primitives/button.mjs +56 -0
- package/dist/ui/primitives/button.mjs.map +1 -0
- package/dist/ui/primitives/card.d.mts +10 -0
- package/dist/ui/primitives/card.d.ts +10 -0
- package/dist/ui/primitives/card.js +101 -0
- package/dist/ui/primitives/card.js.map +1 -0
- package/dist/ui/primitives/card.mjs +62 -0
- package/dist/ui/primitives/card.mjs.map +1 -0
- package/dist/ui/primitives/carousel.d.mts +45 -0
- package/dist/ui/primitives/carousel.d.ts +45 -0
- package/dist/ui/primitives/carousel.js +300 -0
- package/dist/ui/primitives/carousel.js.map +1 -0
- package/dist/ui/primitives/carousel.mjs +261 -0
- package/dist/ui/primitives/carousel.mjs.map +1 -0
- package/dist/ui/primitives/chart.d.mts +68 -0
- package/dist/ui/primitives/chart.d.ts +68 -0
- package/dist/ui/primitives/chart.js +280 -0
- package/dist/ui/primitives/chart.js.map +1 -0
- package/dist/ui/primitives/chart.mjs +241 -0
- package/dist/ui/primitives/chart.mjs.map +1 -0
- package/dist/ui/primitives/collapsible.d.mts +8 -0
- package/dist/ui/primitives/collapsible.d.ts +8 -0
- package/dist/ui/primitives/collapsible.js +72 -0
- package/dist/ui/primitives/collapsible.js.map +1 -0
- package/dist/ui/primitives/collapsible.mjs +36 -0
- package/dist/ui/primitives/collapsible.mjs.map +1 -0
- package/dist/ui/primitives/context-menu.d.mts +29 -0
- package/dist/ui/primitives/context-menu.d.ts +29 -0
- package/dist/ui/primitives/context-menu.js +203 -0
- package/dist/ui/primitives/context-menu.js.map +1 -0
- package/dist/ui/primitives/context-menu.mjs +155 -0
- package/dist/ui/primitives/context-menu.mjs.map +1 -0
- package/dist/ui/primitives/dialog.d.mts +26 -0
- package/dist/ui/primitives/dialog.d.ts +26 -0
- package/dist/ui/primitives/dialog.js +192 -0
- package/dist/ui/primitives/dialog.js.map +1 -0
- package/dist/ui/primitives/dialog.mjs +148 -0
- package/dist/ui/primitives/dialog.mjs.map +1 -0
- package/dist/ui/primitives/drawer.d.mts +26 -0
- package/dist/ui/primitives/drawer.d.ts +26 -0
- package/dist/ui/primitives/drawer.js +146 -0
- package/dist/ui/primitives/drawer.js.map +1 -0
- package/dist/ui/primitives/drawer.mjs +103 -0
- package/dist/ui/primitives/drawer.mjs.map +1 -0
- package/dist/ui/primitives/input.d.mts +5 -0
- package/dist/ui/primitives/input.d.ts +5 -0
- package/dist/ui/primitives/input.js +46 -0
- package/dist/ui/primitives/input.js.map +1 -0
- package/dist/ui/primitives/input.mjs +22 -0
- package/dist/ui/primitives/input.mjs.map +1 -0
- package/dist/ui/primitives/item.d.mts +27 -0
- package/dist/ui/primitives/item.d.ts +27 -0
- package/dist/ui/primitives/item.js +221 -0
- package/dist/ui/primitives/item.js.map +1 -0
- package/dist/ui/primitives/item.mjs +188 -0
- package/dist/ui/primitives/item.mjs.map +1 -0
- package/dist/ui/primitives/kbd.d.mts +15 -0
- package/dist/ui/primitives/kbd.d.ts +15 -0
- package/dist/ui/primitives/kbd.js +45 -0
- package/dist/ui/primitives/kbd.js.map +1 -0
- package/dist/ui/primitives/kbd.mjs +21 -0
- package/dist/ui/primitives/kbd.mjs.map +1 -0
- package/dist/ui/primitives/label.d.mts +6 -0
- package/dist/ui/primitives/label.d.ts +6 -0
- package/dist/ui/primitives/label.js +55 -0
- package/dist/ui/primitives/label.js.map +1 -0
- package/dist/ui/primitives/label.mjs +21 -0
- package/dist/ui/primitives/label.mjs.map +1 -0
- package/dist/ui/primitives/resizable.d.mts +10 -0
- package/dist/ui/primitives/resizable.d.ts +10 -0
- package/dist/ui/primitives/resizable.js +68 -0
- package/dist/ui/primitives/resizable.js.map +1 -0
- package/dist/ui/primitives/resizable.mjs +42 -0
- package/dist/ui/primitives/resizable.mjs.map +1 -0
- package/dist/ui/primitives/save-status.d.mts +31 -0
- package/dist/ui/primitives/save-status.d.ts +31 -0
- package/dist/ui/primitives/save-status.js +150 -0
- package/dist/ui/primitives/save-status.js.map +1 -0
- package/dist/ui/primitives/save-status.mjs +126 -0
- package/dist/ui/primitives/save-status.mjs.map +1 -0
- package/dist/ui/primitives/separator.d.mts +6 -0
- package/dist/ui/primitives/separator.d.ts +6 -0
- package/dist/ui/primitives/separator.js +62 -0
- package/dist/ui/primitives/separator.js.map +1 -0
- package/dist/ui/primitives/separator.mjs +28 -0
- package/dist/ui/primitives/separator.mjs.map +1 -0
- package/dist/ui/primitives/sheet.d.mts +16 -0
- package/dist/ui/primitives/sheet.d.ts +16 -0
- package/dist/ui/primitives/sheet.js +170 -0
- package/dist/ui/primitives/sheet.js.map +1 -0
- package/dist/ui/primitives/sheet.mjs +129 -0
- package/dist/ui/primitives/sheet.mjs.map +1 -0
- package/dist/ui/primitives/sidebar.d.mts +74 -0
- package/dist/ui/primitives/sidebar.d.ts +74 -0
- package/dist/ui/primitives/sidebar.js +697 -0
- package/dist/ui/primitives/sidebar.js.map +1 -0
- package/dist/ui/primitives/sidebar.mjs +651 -0
- package/dist/ui/primitives/sidebar.mjs.map +1 -0
- package/dist/ui/primitives/skeleton.d.mts +5 -0
- package/dist/ui/primitives/skeleton.d.ts +5 -0
- package/dist/ui/primitives/skeleton.js +40 -0
- package/dist/ui/primitives/skeleton.js.map +1 -0
- package/dist/ui/primitives/skeleton.mjs +16 -0
- package/dist/ui/primitives/skeleton.mjs.map +1 -0
- package/dist/ui/primitives/slider.d.mts +20 -0
- package/dist/ui/primitives/slider.d.ts +20 -0
- package/dist/ui/primitives/slider.js +112 -0
- package/dist/ui/primitives/slider.js.map +1 -0
- package/dist/ui/primitives/slider.mjs +78 -0
- package/dist/ui/primitives/slider.mjs.map +1 -0
- package/dist/ui/primitives/sortable.d.mts +50 -0
- package/dist/ui/primitives/sortable.d.ts +50 -0
- package/dist/ui/primitives/sortable.js +456 -0
- package/dist/ui/primitives/sortable.js.map +1 -0
- package/dist/ui/primitives/sortable.mjs +435 -0
- package/dist/ui/primitives/sortable.mjs.map +1 -0
- package/dist/ui/primitives/switch.d.mts +6 -0
- package/dist/ui/primitives/switch.d.ts +6 -0
- package/dist/ui/primitives/switch.js +67 -0
- package/dist/ui/primitives/switch.js.map +1 -0
- package/dist/ui/primitives/switch.mjs +33 -0
- package/dist/ui/primitives/switch.mjs.map +1 -0
- package/dist/ui/primitives/table.d.mts +12 -0
- package/dist/ui/primitives/table.d.ts +12 -0
- package/dist/ui/primitives/table.js +130 -0
- package/dist/ui/primitives/table.js.map +1 -0
- package/dist/ui/primitives/table.mjs +89 -0
- package/dist/ui/primitives/table.mjs.map +1 -0
- package/dist/ui/primitives/tabs.d.mts +9 -0
- package/dist/ui/primitives/tabs.d.ts +9 -0
- package/dist/ui/primitives/tabs.js +86 -0
- package/dist/ui/primitives/tabs.js.map +1 -0
- package/dist/ui/primitives/tabs.mjs +49 -0
- package/dist/ui/primitives/tabs.mjs.map +1 -0
- package/dist/ui/primitives/textarea.d.mts +5 -0
- package/dist/ui/primitives/textarea.d.ts +5 -0
- package/dist/ui/primitives/textarea.js +43 -0
- package/dist/ui/primitives/textarea.js.map +1 -0
- package/dist/ui/primitives/textarea.mjs +19 -0
- package/dist/ui/primitives/textarea.mjs.map +1 -0
- package/dist/ui/primitives/tooltip.d.mts +9 -0
- package/dist/ui/primitives/tooltip.d.ts +9 -0
- package/dist/ui/primitives/tooltip.js +94 -0
- package/dist/ui/primitives/tooltip.js.map +1 -0
- package/dist/ui/primitives/tooltip.mjs +57 -0
- package/dist/ui/primitives/tooltip.mjs.map +1 -0
- package/docs/agents.md +72 -29
- package/docs/api/README.md +7 -3
- package/docs/api/appObjects.d.ts +3 -1
- package/docs/api/appObjects.md +2 -1
- package/docs/api/by-path.md +51 -0
- package/docs/api/dataPipelines.d.ts +149 -0
- package/docs/api/dataPipelines.md +232 -0
- package/docs/api/forms.d.ts +16 -0
- package/docs/api/forms.md +8 -0
- package/docs/api/objects.d.ts +705 -5
- package/docs/api/objects.md +1099 -14
- package/docs/api/ops.d.ts +1 -1
- package/docs/api/ops.md +1 -1
- package/docs/api/processLists.d.ts +247 -0
- package/docs/api/processLists.md +409 -0
- package/docs/api/quotes.d.ts +3 -1
- package/docs/api/quotes.md +7 -2
- package/docs/api/radar.d.ts +4 -0
- package/docs/api/radar.md +1 -0
- package/docs/api/reports.d.ts +476 -0
- package/docs/api/reports.md +539 -0
- package/docs/api/scheduleBuy.d.ts +12 -8
- package/docs/api/scheduleBuy.md +13 -10
- package/docs/api/seo.d.ts +406 -3
- package/docs/api/seo.md +505 -1
- package/docs/api/workspaceBackup.d.ts +324 -0
- package/docs/api/workspaceBackup.md +427 -0
- package/package.json +174 -5
- package/stacks/6sense.json +46 -0
- package/stacks/README.md +20 -0
- package/stacks/apollo.json +128 -0
- package/stacks/clay.json +59 -0
- package/stacks/gong.json +72 -0
- package/stacks/hubspot.json +162 -0
- package/stacks/intercom.json +53 -0
- package/stacks/mailchimp.json +71 -0
- package/stacks/outreach.json +86 -0
- package/stacks/salesforce.json +94 -0
- package/stacks/stack.schema.json +32 -0
- package/stacks/starter.json +22 -0
- package/styles/styles.css +2 -0
- package/styles/theme.css +181 -0
- package/styles/tokens.css +284 -0
- package/templates/next-app/README.md.tmpl +35 -0
- package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
- package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
- package/templates/next-app/app/globals.css.tmpl +8 -0
- package/templates/next-app/app/layout.tsx.tmpl +19 -0
- package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
- package/templates/next-app/dot-env.local.example.tmpl +6 -0
- package/templates/next-app/dot-gitignore.tmpl +7 -0
- package/templates/next-app/lib/nav.ts.tmpl +2 -0
- package/templates/next-app/next.config.mjs.tmpl +4 -0
- package/templates/next-app/package.json.tmpl +25 -0
- package/templates/next-app/postcss.config.mjs.tmpl +4 -0
- package/templates/next-app/tsconfig.json.tmpl +21 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# search: prospect search
|
|
2
|
+
|
|
3
|
+
Find new people and companies in graph8's open-data index with filters, preview them, and save the ones you
|
|
4
|
+
want into a list. Four components: a filter builder, a results table, a save-to-list dialog and saved
|
|
5
|
+
searches. Builds on `lists` (which builds on `crm`): once results are saved, you work with real contacts.
|
|
6
|
+
|
|
7
|
+
Install: `npx @graph8/sdk add search` (installs `foundation`, `crm` and `lists` first).
|
|
8
|
+
|
|
9
|
+
## Read this first: search results are masked
|
|
10
|
+
|
|
11
|
+
`POST /search/contacts` returns **previews**, not CRM records:
|
|
12
|
+
|
|
13
|
+
- a locked email or phone arrives as `"***"` and its name is listed in `locked_fields`; `null` means no value;
|
|
14
|
+
- there is **no contact id** in a result, so a search row cannot be emailed, enrolled or edited;
|
|
15
|
+
- the way to unlock them is to **save the results into a list** (or enrich them).
|
|
16
|
+
|
|
17
|
+
So the order is always: **search (preview) -> save to a new list -> wait for the list to finish building ->
|
|
18
|
+
read the list's contacts (real, unlocked, with ids) -> enrich/verify -> enroll.** The components enforce it:
|
|
19
|
+
locked cells render as **Locked**, never as the placeholder, and `ProspectResults` tells the person how many are
|
|
20
|
+
locked and that saving unlocks them. Do not build a screen that treats `"***"` as an email.
|
|
21
|
+
|
|
22
|
+
## What you get
|
|
23
|
+
|
|
24
|
+
| Component | Use it for | Props |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `FilterBuilder` | Field / operator / values rows. Enumerated fields offer the real value set, names type ahead | `resource`, `value`, `onChange`, `onSearch` |
|
|
27
|
+
| `ProspectResults` | Run the search, show one page, total, paging, locked banner, open the save dialog | `resource`, `filters`, `pageSize`, `confirmSearch`, `creditsPerRecord`, `onSaved` |
|
|
28
|
+
| `SaveToListDialog` | Save all matches into a NEW list (confirmed), poll until built | `open`, `onOpenChange`, `resource`, `filters`, `estimatedTotal`, `onSaved` |
|
|
29
|
+
| `SavedSearches` | Name and keep filters, load, delete | `resource`, `current`, `onLoad` |
|
|
30
|
+
|
|
31
|
+
`ProspectResults` with NO `filters` prop is a complete prospecting page (builder, saved searches, results): mount it as a
|
|
32
|
+
route as is. With `filters` it renders just the results.
|
|
33
|
+
|
|
34
|
+
Hook: `useFilterOptions(resource, field)`. Helpers in `lib/search-types.ts`: `completeFilters`, `isLocked`,
|
|
35
|
+
`visible`, the field catalog.
|
|
36
|
+
|
|
37
|
+
## The flow, with the ids that pass between steps
|
|
38
|
+
|
|
39
|
+
1. `GET /search/filter-options?resource=contacts&fields=company_industry` -> `{ company_industry: [{ id, label, count }] }`.
|
|
40
|
+
Send the **`id`** back as the filter value.
|
|
41
|
+
2. `POST /search/contacts { filters: [{ field, operator, value: [...] }], page, limit }` ->
|
|
42
|
+
`{ data: [...], pagination: { total, total_available, has_next, notes } }`. Filters are ANDed. At least one is
|
|
43
|
+
required (an unfiltered search is refused). `page * limit` may not exceed 10,000.
|
|
44
|
+
3. `POST /search/contacts/save { filters, list_title, max_results }` -> **202** `{ list_id, list_title, estimated_total, status: "processing" }`.
|
|
45
|
+
Shown behind `ConfirmSpendDialog` (billable). 402 means a trial contact cap was reached.
|
|
46
|
+
4. Poll `GET /lists?search=<list_title>` until the list with that `list_id` has `status: "completed"` (or `"failed"`).
|
|
47
|
+
5. Read the members: `GET /contacts?list_id=<list_id>` (`ContactTable listId`). Emails and phones are now real.
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
"use client";
|
|
51
|
+
import { useState } from "react";
|
|
52
|
+
import { FilterBuilder } from "@/components/g8/search/filter-builder";
|
|
53
|
+
import { ProspectResults } from "@/components/g8/search/prospect-results";
|
|
54
|
+
import { SavedSearches } from "@/components/g8/search/saved-searches";
|
|
55
|
+
import type { SearchCondition } from "@/components/g8/search/lib/search-types";
|
|
56
|
+
|
|
57
|
+
export default function Prospecting() {
|
|
58
|
+
const [draft, setDraft] = useState<SearchCondition[]>([]);
|
|
59
|
+
const [applied, setApplied] = useState<SearchCondition[]>([]);
|
|
60
|
+
return (
|
|
61
|
+
<div className="grid gap-4 lg:grid-cols-[22rem_1fr]">
|
|
62
|
+
<div className="flex flex-col gap-4">
|
|
63
|
+
<FilterBuilder value={draft} onChange={setDraft} onSearch={setApplied} />
|
|
64
|
+
<SavedSearches current={draft} onLoad={(filters) => { setDraft(filters); setApplied(filters); }} />
|
|
65
|
+
</div>
|
|
66
|
+
<ProspectResults filters={applied} onSaved={(list) => (window.location.href = `/lists/${list.id}`)} />
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
(`add` flattens `components/` into `components/g8/search/` (or `src/components/g8/search/`); `lib/` and `hooks/`
|
|
73
|
+
keep their folders under it. Adjust the alias to your app.)
|
|
74
|
+
|
|
75
|
+
## Tiers and confirmations
|
|
76
|
+
|
|
77
|
+
- Searching, filter options, autocomplete and listing saved searches are reads. The contract labels
|
|
78
|
+
`searchContacts` / `searchCompanies` **billable**, but the API router and the SDK docs both state that searching
|
|
79
|
+
does **not** consume credits; so `ProspectResults` searches on the person's click without a dialog. Pass
|
|
80
|
+
`confirmSearch` to ask first anyway. (If graph8 ever starts charging for search, flip that default.)
|
|
81
|
+
- **Saving to a list is the spend.** `SaveToListDialog` always goes through `ConfirmSpendDialog` (billable) and shows
|
|
82
|
+
how many records. There is no balance or price read in the API, so an estimate appears only if you pass `creditsPerRecord`.
|
|
83
|
+
- Deleting a saved search is destructive and confirmed.
|
|
84
|
+
|
|
85
|
+
## Prerequisites
|
|
86
|
+
|
|
87
|
+
- `lists-installed` (app): the CLI installs `lists` and `crm`.
|
|
88
|
+
- `proxy-allowlist` (app): `allowedPrefixes` includes `search` and `lists`.
|
|
89
|
+
- `active-subscription` (api): search and save refuse a workspace without one.
|
|
90
|
+
|
|
91
|
+
## Defaults the agent can use without asking
|
|
92
|
+
|
|
93
|
+
| Decision | Default |
|
|
94
|
+
|---|---|
|
|
95
|
+
| Confirm before each search | no (free); `confirmSearch` to opt in |
|
|
96
|
+
| Records a save takes | min(matches, 1000), editable up to 10,000 |
|
|
97
|
+
| New list name | `Search <date>` |
|
|
98
|
+
|
|
99
|
+
Ask the person: who may save searches into the CRM (it spends credits) - decide it in the proxy's `authorize`.
|
|
100
|
+
|
|
101
|
+
## Gotchas
|
|
102
|
+
|
|
103
|
+
- The save endpoint can only create a **new** list. To put chosen people into an existing list, save first, then
|
|
104
|
+
use `AddToListButton` (lists block) on the new list's members.
|
|
105
|
+
- Saved searches made in the graph8 app use a different stored format; they are listed but cannot be loaded here.
|
|
106
|
+
Searches saved by this block carry `icp_query.format = "g8-sdk-v1"`.
|
|
107
|
+
- `between` takes exactly two values; value-less operators (`is_empty`, `is_not_empty`, `exists`) take none.
|
|
108
|
+
- If a Radix dialog or sheet is open, render `ConfirmSpendDialog` inside it (a modal makes the rest of the page inert).
|
|
109
|
+
|
|
110
|
+
## Recipes
|
|
111
|
+
|
|
112
|
+
| File | Does |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `recipes/search-and-save-to-list.ts` | Preview (ignoring masked emails), save, poll the list, read unlocked members |
|
|
115
|
+
| `recipes/explore-filter-values.ts` | Valid filter values, type-ahead, values within a subset |
|
|
116
|
+
| `recipes/saved-searches.ts` | Save, list, delete saved searches |
|
|
117
|
+
|
|
118
|
+
## Operations
|
|
119
|
+
|
|
120
|
+
| Operation | Method and path | Tier | Used by |
|
|
121
|
+
|---|---|---|---|
|
|
122
|
+
| searchContacts / searchCompanies | POST /search/contacts, /search/companies | billable (see above: free in practice) | ProspectResults |
|
|
123
|
+
| saveContactSearch / saveCompanySearch | POST /search/contacts/save, /search/companies/save | billable | SaveToListDialog |
|
|
124
|
+
| listLists | GET /lists | read | SaveToListDialog (polling) |
|
|
125
|
+
| getFilterOptions | GET /search/filter-options | read | FilterBuilder |
|
|
126
|
+
| getAutocompleteSuggestions | GET /search/autocomplete | read | FilterBuilder |
|
|
127
|
+
| getFilteredFilterOptions | POST /search/filter-options | read | recipe |
|
|
128
|
+
| listSavedSearches / saveSearch | GET, POST /search/saved-searches | read / write | SavedSearches |
|
|
129
|
+
| deleteSavedSearch | DELETE /search/saved-searches/{query_id} | destructive | SavedSearches |
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../block.schema.json",
|
|
3
|
+
"id": "search",
|
|
4
|
+
"title": "Prospect search",
|
|
5
|
+
"summary": "Filter builder with real value suggestions, paged prospect results that handle locked fields, save-to-list that reveals them, and saved searches.",
|
|
6
|
+
"features": [
|
|
7
|
+
"filter builder with value suggestions from the real value sets and type-ahead",
|
|
8
|
+
"paged contact and company results with total and paging-window handling",
|
|
9
|
+
"locked emails and phones shown as Locked, with a one-step path to reveal them",
|
|
10
|
+
"save-to-list into a new list with a billable confirmation and polled progress",
|
|
11
|
+
"saved searches: save, load, delete"
|
|
12
|
+
],
|
|
13
|
+
"dependsOn": [
|
|
14
|
+
"lists"
|
|
15
|
+
],
|
|
16
|
+
"operations": [
|
|
17
|
+
"delete_saved_search_search_saved_searches__query_id__delete",
|
|
18
|
+
"get_autocomplete_suggestions_search_autocomplete_get",
|
|
19
|
+
"get_filter_options_search_filter_options_get",
|
|
20
|
+
"get_filtered_filter_options_search_filter_options_post",
|
|
21
|
+
"list_lists_lists_get",
|
|
22
|
+
"list_saved_searches_search_saved_searches_get",
|
|
23
|
+
"save_company_search_search_companies_save_post",
|
|
24
|
+
"save_contact_search_search_contacts_save_post",
|
|
25
|
+
"save_search_search_saved_searches_post",
|
|
26
|
+
"search_companies_search_companies_post",
|
|
27
|
+
"search_contacts_search_contacts_post"
|
|
28
|
+
],
|
|
29
|
+
"components": [
|
|
30
|
+
{
|
|
31
|
+
"name": "FilterBuilder",
|
|
32
|
+
"file": "components/filter-builder.tsx",
|
|
33
|
+
"purpose": "Build the filter list: field, operator, values. Enumerated fields offer their real values, names type ahead. Controlled; fires onSearch with the complete filters.",
|
|
34
|
+
"operations": [
|
|
35
|
+
"get_filter_options_search_filter_options_get",
|
|
36
|
+
"get_autocomplete_suggestions_search_autocomplete_get"
|
|
37
|
+
],
|
|
38
|
+
"tiers": [
|
|
39
|
+
"read"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "ProspectResults",
|
|
44
|
+
"file": "components/prospect-results.tsx",
|
|
45
|
+
"purpose": "Run the search (or, with no filters prop, a whole prospecting page with filters and saved searches) and show one page of matches with total, paging window and a locked-field banner; opens SaveToListDialog. Optional confirm before each search.",
|
|
46
|
+
"operations": [
|
|
47
|
+
"search_contacts_search_contacts_post",
|
|
48
|
+
"search_companies_search_companies_post",
|
|
49
|
+
"get_filter_options_search_filter_options_get",
|
|
50
|
+
"get_autocomplete_suggestions_search_autocomplete_get",
|
|
51
|
+
"list_saved_searches_search_saved_searches_get",
|
|
52
|
+
"save_search_search_saved_searches_post",
|
|
53
|
+
"delete_saved_search_search_saved_searches__query_id__delete",
|
|
54
|
+
"list_lists_lists_get",
|
|
55
|
+
"save_contact_search_search_contacts_save_post",
|
|
56
|
+
"save_company_search_search_companies_save_post"
|
|
57
|
+
],
|
|
58
|
+
"tiers": [
|
|
59
|
+
"read",
|
|
60
|
+
"write",
|
|
61
|
+
"billable",
|
|
62
|
+
"destructive"
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "SaveToListDialog",
|
|
67
|
+
"file": "components/save-to-list-dialog.tsx",
|
|
68
|
+
"purpose": "Save everything a search matches into a NEW list (billable, confirmed), then poll the list until it is built. This is how locked emails and phones are revealed.",
|
|
69
|
+
"operations": [
|
|
70
|
+
"list_lists_lists_get",
|
|
71
|
+
"save_contact_search_search_contacts_save_post",
|
|
72
|
+
"save_company_search_search_companies_save_post"
|
|
73
|
+
],
|
|
74
|
+
"tiers": [
|
|
75
|
+
"read",
|
|
76
|
+
"billable"
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"name": "SavedSearches",
|
|
81
|
+
"file": "components/saved-searches.tsx",
|
|
82
|
+
"purpose": "Save the current filters under a name, load one back into the builder, delete the ones no longer needed.",
|
|
83
|
+
"operations": [
|
|
84
|
+
"list_saved_searches_search_saved_searches_get",
|
|
85
|
+
"save_search_search_saved_searches_post",
|
|
86
|
+
"delete_saved_search_search_saved_searches__query_id__delete"
|
|
87
|
+
],
|
|
88
|
+
"tiers": [
|
|
89
|
+
"read",
|
|
90
|
+
"write",
|
|
91
|
+
"destructive"
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"hooks": [
|
|
96
|
+
{
|
|
97
|
+
"name": "useFilterOptions",
|
|
98
|
+
"file": "hooks/use-filter-options.ts",
|
|
99
|
+
"purpose": "The distinct values of a searchable field, cached per resource and field.",
|
|
100
|
+
"operations": [
|
|
101
|
+
"get_filter_options_search_filter_options_get"
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
"lib": [
|
|
106
|
+
{
|
|
107
|
+
"file": "lib/search-types.ts",
|
|
108
|
+
"purpose": "Filter vocabulary (fields, operators, kinds), result shapes and the locked-field helpers (isLocked, visible)."
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
"recipes": [
|
|
112
|
+
{
|
|
113
|
+
"file": "recipes/search-and-save-to-list.ts",
|
|
114
|
+
"title": "Preview a prospect search, save it to a list and read the unlocked contacts",
|
|
115
|
+
"when": "Building an audience from the open-data index. Search results carry no contact id and mask locked emails and phones as \"***\" (named in `locked_fields`): never treat \"***\" as a real email. Order: search (preview) -> save to a new list -> wait for the list to finish building -> read the list's contacts, which are real, unlocked records with ids -> enrich/verify -> enroll. Saving can spend credits, so ask a human first."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"file": "recipes/explore-filter-values.ts",
|
|
119
|
+
"title": "Look up valid filter values before searching",
|
|
120
|
+
"when": "Writing a filter on a field with a fixed value set (industry, seniority, country, company size) or a name you have to spell exactly (company, domain). A guessed value returns zero rows, indistinguishable from a correct filter that matches nothing. The `id` of each option is exactly what the search expects as a filter value."
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"file": "recipes/saved-searches.ts",
|
|
124
|
+
"title": "Save, list and delete saved searches",
|
|
125
|
+
"when": "A search is run again and again (a weekly ICP pull). A saved search belongs to the API key's own identity, not the whole org. `icp_query` is stored as-is: keep your own format marker so you can load it back."
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"prerequisites": [
|
|
129
|
+
{
|
|
130
|
+
"id": "lists-installed",
|
|
131
|
+
"where": "app",
|
|
132
|
+
"description": "The lists block (and crm) are installed; the CLI does it. Search results are worked with after they are saved into a list.",
|
|
133
|
+
"verify": "components/g8/lists/ and components/g8/crm/ exist."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": "proxy-allowlist",
|
|
137
|
+
"where": "app",
|
|
138
|
+
"description": "allowedPrefixes in the /api/g8 route includes `search` and `lists`.",
|
|
139
|
+
"verify": "POST /api/g8/search/contacts with a filter returns 200 when signed in."
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"id": "active-subscription",
|
|
143
|
+
"where": "api",
|
|
144
|
+
"description": "The graph8 workspace has an active subscription (search and save endpoints refuse otherwise). Trial workspaces have a contact cap: a save over the cap answers 402.",
|
|
145
|
+
"verify": "A small search returns results; a save of a few contacts returns 202."
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
"scopes": [
|
|
149
|
+
"lists:read",
|
|
150
|
+
"search:delete",
|
|
151
|
+
"search:read",
|
|
152
|
+
"search:run",
|
|
153
|
+
"search:write"
|
|
154
|
+
],
|
|
155
|
+
"defaults": [
|
|
156
|
+
{
|
|
157
|
+
"decision": "Whether searching asks for confirmation",
|
|
158
|
+
"default": "No: searching does not consume credits (router and SDK docs). Pass confirmSearch to ProspectResults to ask anyway",
|
|
159
|
+
"askHuman": false
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"decision": "How many records a save takes",
|
|
163
|
+
"default": "min(matches, 1000); the person can raise it up to 10,000",
|
|
164
|
+
"askHuman": false
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"decision": "Name of the list a search is saved into",
|
|
168
|
+
"default": "Search <date>",
|
|
169
|
+
"askHuman": false
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"decision": "Credits per saved record shown in the confirmation",
|
|
173
|
+
"default": "Not shown (no balance or price read exists); pass creditsPerRecord if the plan's rate is known",
|
|
174
|
+
"askHuman": false
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"decision": "Who may save searches into the CRM (spends credits)",
|
|
178
|
+
"default": "Anyone the proxy's authorize lets through",
|
|
179
|
+
"askHuman": true
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
"peerDependencies": {
|
|
183
|
+
"react": ">=18"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FilterBuilder: build the filter list a prospect search takes. One row per condition: field,
|
|
5
|
+
* operator, values. Enumerated fields (seniority, industry, country...) offer their real value set,
|
|
6
|
+
* company name/domain/job title type ahead, so nobody guesses a spelling that silently returns nothing.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order (all read, free; no confirm dialog):
|
|
9
|
+
* 1. getFilterOptions GET /search/filter-options?resource&fields=... the values of an enumerated field
|
|
10
|
+
* 2. getAutocompleteSuggestions GET /search/autocomplete?resource&field&value type-ahead for names, domains, titles
|
|
11
|
+
*
|
|
12
|
+
* Controlled component: it never runs a search. `onSearch` is called when the person clicks Search; hand
|
|
13
|
+
* `completeFilters(value)` to ProspectResults. Incomplete rows are ignored by the search, not sent.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useEffect, useState } from "react";
|
|
17
|
+
import { Button, Input } from "@graph8/sdk/ui";
|
|
18
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
19
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
20
|
+
import { useFilterOptions } from "../hooks/use-filter-options";
|
|
21
|
+
import { NO_VALUE_OPERATORS, OPERATORS_BY_KIND, OPERATOR_LABEL, completeFilters, fieldsFor } from "../lib/search-types";
|
|
22
|
+
import type { FieldMeta, SearchCondition, SearchOperator, SearchResource } from "../lib/search-types";
|
|
23
|
+
|
|
24
|
+
export interface FilterBuilderProps {
|
|
25
|
+
resource?: SearchResource;
|
|
26
|
+
value: SearchCondition[];
|
|
27
|
+
onChange: (filters: SearchCondition[]) => void;
|
|
28
|
+
/** Search button. Without it the builder is just a form. */
|
|
29
|
+
onSearch?: (filters: SearchCondition[]) => void;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Pull strings out of the open autocomplete envelope, whatever list key it uses. */
|
|
35
|
+
export function extractSuggestions(data: unknown): string[] {
|
|
36
|
+
const list = Array.isArray(data) ? data : ((data as Record<string, unknown> | null) && Object.values(data as Record<string, unknown>).find(Array.isArray)) || [];
|
|
37
|
+
return (list as unknown[])
|
|
38
|
+
.map((item) => (typeof item === "string" ? item : String((item as Record<string, unknown>)?.label ?? (item as Record<string, unknown>)?.value ?? (item as Record<string, unknown>)?.name ?? "")))
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.slice(0, 8);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Stable row identity across edits (update() keeps the object's id), so removing a row never hands its draft text to the next one.
|
|
44
|
+
const ids = new WeakMap<object, number>();
|
|
45
|
+
let nextId = 0;
|
|
46
|
+
function rowId(c: SearchCondition): number {
|
|
47
|
+
if (!ids.has(c)) ids.set(c, nextId++);
|
|
48
|
+
return ids.get(c)!;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function useDebounced<T>(value: T, ms = 250): T {
|
|
52
|
+
const [v, setV] = useState(value);
|
|
53
|
+
useEffect(() => { const t = setTimeout(() => setV(value), ms); return () => clearTimeout(t); }, [value, ms]);
|
|
54
|
+
return v;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ValuePicker({ resource, meta, value, onChange, disabled }: { resource: SearchResource; meta: FieldMeta; value: unknown[]; onChange: (v: unknown[]) => void; disabled?: boolean }) {
|
|
58
|
+
const [draft, setDraft] = useState("");
|
|
59
|
+
const typed = useDebounced(draft.trim());
|
|
60
|
+
const options = useFilterOptions(resource, meta.key, meta.suggest === "options");
|
|
61
|
+
const auto = useG8Query(
|
|
62
|
+
async (signal) => extractSuggestions((await g8Get("/search/autocomplete", { resource, field: meta.key, value: typed, size: 8 }, { signal })).data),
|
|
63
|
+
[resource, meta.key, typed],
|
|
64
|
+
{ enabled: meta.suggest === "autocomplete" && typed.length >= 2 },
|
|
65
|
+
);
|
|
66
|
+
const labels = new Map(options.options.map((o) => [o.id, o.label]));
|
|
67
|
+
const suggestions = meta.suggest === "options"
|
|
68
|
+
? options.options.filter((o) => !value.includes(o.id) && o.label.toLowerCase().includes(draft.toLowerCase())).slice(0, 8).map((o) => o.id)
|
|
69
|
+
: meta.suggest === "autocomplete" && typed.length >= 2 ? auto.data ?? [] : [];
|
|
70
|
+
|
|
71
|
+
const add = (v: string) => {
|
|
72
|
+
const clean = v.trim();
|
|
73
|
+
if (clean && !value.includes(clean)) onChange([...value, clean]);
|
|
74
|
+
setDraft("");
|
|
75
|
+
};
|
|
76
|
+
return (
|
|
77
|
+
<div className="flex min-w-48 flex-1 flex-col gap-1">
|
|
78
|
+
<div className="flex flex-wrap items-center gap-1">
|
|
79
|
+
{value.map((v) => (
|
|
80
|
+
<span key={String(v)} className="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs">
|
|
81
|
+
{labels.get(String(v)) ?? String(v)}
|
|
82
|
+
<button type="button" aria-label={`Remove ${String(v)}`} disabled={disabled} onClick={() => onChange(value.filter((x) => x !== v))}>x</button>
|
|
83
|
+
</span>
|
|
84
|
+
))}
|
|
85
|
+
<Input aria-label={`${meta.label} value`} className="h-8 w-40" placeholder={meta.kind === "number" ? "e.g. 2015" : "Type and press Enter"} value={draft} disabled={disabled}
|
|
86
|
+
onChange={(e) => setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); add(draft); } }} />
|
|
87
|
+
</div>
|
|
88
|
+
{suggestions.length > 0 ? (
|
|
89
|
+
<ul aria-label="Suggestions" className="flex flex-wrap gap-1">
|
|
90
|
+
{suggestions.map((s) => (
|
|
91
|
+
<li key={s}><button type="button" className="rounded border px-2 py-0.5 text-xs hover:bg-accent" onClick={() => add(s)}>{labels.get(s) ?? s}</button></li>
|
|
92
|
+
))}
|
|
93
|
+
</ul>
|
|
94
|
+
) : null}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function BetweenPicker({ meta, value, onChange, disabled }: { meta: FieldMeta; value: unknown[]; onChange: (v: unknown[]) => void; disabled?: boolean }) {
|
|
100
|
+
return (
|
|
101
|
+
<div className="flex items-center gap-2">
|
|
102
|
+
<Input aria-label={`${meta.label} from`} className="h-8 w-24" inputMode="numeric" disabled={disabled} value={String(value[0] ?? "")} onChange={(e) => onChange([e.target.value, value[1] ?? ""])} />
|
|
103
|
+
<span className="text-sm text-muted-foreground">and</span>
|
|
104
|
+
<Input aria-label={`${meta.label} to`} className="h-8 w-24" inputMode="numeric" disabled={disabled} value={String(value[1] ?? "")} onChange={(e) => onChange([value[0] ?? "", e.target.value])} />
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function FilterBuilder({ resource = "contacts", value, onChange, onSearch, disabled, className }: FilterBuilderProps) {
|
|
110
|
+
const fields = fieldsFor(resource);
|
|
111
|
+
const meta = (key: string) => fields.find((f) => f.key === key) ?? fields[0];
|
|
112
|
+
const ready = completeFilters(value);
|
|
113
|
+
const update = (i: number, patch: Partial<SearchCondition>) => onChange(value.map((c, j) => { if (j !== i) return c; const n = { ...c, ...patch }; ids.set(n, rowId(c)); return n; }));
|
|
114
|
+
|
|
115
|
+
const changeField = (i: number, key: string) => {
|
|
116
|
+
const m = meta(key);
|
|
117
|
+
update(i, { field: key, operator: OPERATORS_BY_KIND[m.kind][0], value: [] });
|
|
118
|
+
};
|
|
119
|
+
const changeOperator = (i: number, operator: SearchOperator) => update(i, { operator, value: NO_VALUE_OPERATORS.includes(operator) ? [] : value[i].value });
|
|
120
|
+
const add = () => onChange([...value, { field: fields[0].key, operator: OPERATORS_BY_KIND[fields[0].kind][0], value: [] }]);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className={`flex flex-col gap-3 ${className ?? ""}`}>
|
|
124
|
+
{value.length === 0 ? <p className="text-sm text-muted-foreground">No filters yet. Add at least one: an unfiltered search is refused.</p> : null}
|
|
125
|
+
{value.map((c, i) => {
|
|
126
|
+
const m = meta(c.field);
|
|
127
|
+
return (
|
|
128
|
+
<div key={rowId(c)} className="flex flex-wrap items-start gap-2" data-testid="filter-row">
|
|
129
|
+
<select aria-label="Field" className="h-8 rounded-md border bg-background px-2 text-sm" value={c.field} disabled={disabled} onChange={(e) => changeField(i, e.target.value)}>
|
|
130
|
+
{fields.map((f) => <option key={f.key} value={f.key}>{f.label}</option>)}
|
|
131
|
+
</select>
|
|
132
|
+
<select aria-label="Operator" className="h-8 rounded-md border bg-background px-2 text-sm" value={c.operator} disabled={disabled} onChange={(e) => changeOperator(i, e.target.value as SearchOperator)}>
|
|
133
|
+
{OPERATORS_BY_KIND[m.kind].map((o) => <option key={o} value={o}>{OPERATOR_LABEL[o]}</option>)}
|
|
134
|
+
</select>
|
|
135
|
+
{NO_VALUE_OPERATORS.includes(c.operator) ? null : c.operator === "between"
|
|
136
|
+
? <BetweenPicker meta={m} value={c.value ?? []} disabled={disabled} onChange={(v) => update(i, { value: v })} />
|
|
137
|
+
: <ValuePicker resource={resource} meta={m} value={c.value ?? []} disabled={disabled} onChange={(v) => update(i, { value: v })} />}
|
|
138
|
+
<Button type="button" variant="ghost" size="sm" disabled={disabled} aria-label="Remove filter" onClick={() => onChange(value.filter((_, j) => j !== i))}>Remove</Button>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
})}
|
|
142
|
+
<div className="flex items-center gap-2">
|
|
143
|
+
<Button type="button" variant="outline" size="sm" disabled={disabled} onClick={add}>Add filter</Button>
|
|
144
|
+
{value.length > 0 ? <Button type="button" variant="ghost" size="sm" disabled={disabled} onClick={() => onChange([])}>Clear all</Button> : null}
|
|
145
|
+
{onSearch ? <Button type="button" className="ml-auto" size="sm" disabled={disabled || ready.length === 0} onClick={() => onSearch(ready)}>Search</Button> : null}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ProspectResults: run a filter search (or, with no `filters` prop, a whole prospecting page) against the open-data index and show one page of matches,
|
|
5
|
+
* with paging, the total, and a "Save to list" action (SaveToListDialog).
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. searchContacts | searchCompanies POST /search/contacts | /search/companies { filters, page, limit }
|
|
9
|
+
* The contract labels this `billable`, but the router and the SDK docs say searching does NOT consume
|
|
10
|
+
* credits, so it runs on the person's click without a dialog. Pass `confirmSearch` to put a
|
|
11
|
+
* ConfirmSpendDialog in front of every search anyway (stricter plans, or if graph8 starts charging).
|
|
12
|
+
* 2. saveContactSearch | saveCompanySearch via SaveToListDialog (billable, confirmed)
|
|
13
|
+
*
|
|
14
|
+
* LOCKED FIELDS: emails and phones the org has not unlocked come back as "***" and are named in
|
|
15
|
+
* `locked_fields`. They render as Locked and are never a value you can copy or send. Results carry NO contact
|
|
16
|
+
* id: you cannot enroll or email a search row directly. Save to a list first, then work with the list's contacts.
|
|
17
|
+
*
|
|
18
|
+
* Paging window: page x limit may not exceed 10,000; narrow the filters to go further.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useState } from "react";
|
|
22
|
+
import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
23
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
24
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
25
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
26
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
27
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
28
|
+
import type { G8Envelope } from "@/g8/lib/g8-client";
|
|
29
|
+
import { SEARCH_WINDOW, completeFilters, isLocked, visible } from "../lib/search-types";
|
|
30
|
+
import type { ProspectCompany, ProspectContact, SearchCondition, SearchPagination, SearchResource } from "../lib/search-types";
|
|
31
|
+
import { FilterBuilder } from "./filter-builder";
|
|
32
|
+
import { SavedSearches } from "./saved-searches";
|
|
33
|
+
import { SaveToListDialog } from "./save-to-list-dialog";
|
|
34
|
+
import type { SavedListInfo } from "./save-to-list-dialog";
|
|
35
|
+
|
|
36
|
+
export interface ProspectResultsProps {
|
|
37
|
+
resource?: SearchResource;
|
|
38
|
+
/**
|
|
39
|
+
* The APPLIED filters (for example from FilterBuilder's onSearch). Nothing runs until at least one is complete.
|
|
40
|
+
* OMIT it and the component is a whole prospecting page: FilterBuilder, SavedSearches and the results.
|
|
41
|
+
*/
|
|
42
|
+
filters?: SearchCondition[];
|
|
43
|
+
pageSize?: number;
|
|
44
|
+
/** Ask for confirmation before every search. Default false (searching is free). */
|
|
45
|
+
confirmSearch?: boolean;
|
|
46
|
+
/** Credits per saved record on your plan, if known, to show an estimate when saving. */
|
|
47
|
+
creditsPerRecord?: number;
|
|
48
|
+
/** Called when a saved list finished building. */
|
|
49
|
+
onSaved?: (list: SavedListInfo) => void;
|
|
50
|
+
className?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type Row = ProspectContact & ProspectCompany;
|
|
54
|
+
|
|
55
|
+
function Locked() {
|
|
56
|
+
return <span className="italic text-muted-foreground" title="Locked: save these results to a list to unlock">Locked</span>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function Cell({ row, field }: { row: ProspectContact; field: keyof ProspectContact }) {
|
|
60
|
+
return isLocked(row, field) ? <Locked /> : <>{visible(row, field) || <span className="text-muted-foreground">-</span>}</>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ResultsTable({ resource = "contacts", filters, pageSize = 25, confirmSearch = false, creditsPerRecord, onSaved, className }: ProspectResultsProps & { filters: SearchCondition[] }) {
|
|
64
|
+
const applied = completeFilters(filters);
|
|
65
|
+
const key = JSON.stringify(applied);
|
|
66
|
+
const [state, setState] = useState({ key, page: 1, approved: !confirmSearch ? key : "" });
|
|
67
|
+
const page = state.key === key ? state.page : 1;
|
|
68
|
+
const approved = confirmSearch ? state.approved === key : true;
|
|
69
|
+
const [saving, setSaving] = useState(false);
|
|
70
|
+
const [asking, setAsking] = useState(false);
|
|
71
|
+
|
|
72
|
+
const search = useG8Query(
|
|
73
|
+
(signal) => g8Post<Row[]>(`/search/${resource}`, { filters: applied, page, limit: pageSize }, { signal }),
|
|
74
|
+
[resource, key, page, pageSize],
|
|
75
|
+
{ enabled: applied.length > 0 && approved },
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (applied.length === 0) return <EmptyState className={className} title="Add a filter to search" description="Searches need at least one filter, for example a job title or an industry." />;
|
|
79
|
+
if (!approved) {
|
|
80
|
+
return (
|
|
81
|
+
<div className={className}>
|
|
82
|
+
<EmptyState title="Ready to search" description="Your plan asks you to confirm each search." action={<Button size="sm" onClick={() => setAsking(true)}>Run search</Button>} />
|
|
83
|
+
<ConfirmSpendDialog open={asking} tier="billable" title="Run this search" description="Searches are confirmed before they run."
|
|
84
|
+
confirmLabel="Search" onConfirm={() => { setAsking(false); setState({ key, page: 1, approved: key }); }} onCancel={() => setAsking(false)} />
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const rows = (search.data?.data ?? []) as Row[];
|
|
90
|
+
const info = (search.data as G8Envelope<Row[]> | undefined)?.pagination as SearchPagination | undefined;
|
|
91
|
+
const available = info?.total_available ?? info?.total ?? rows.length;
|
|
92
|
+
const lowerBound = info?.total_is_lower_bound || (info?.total_available != null && info.total_available > (info?.total ?? 0));
|
|
93
|
+
const maxPage = Math.max(1, Math.floor(SEARCH_WINDOW / pageSize));
|
|
94
|
+
const lockedEmails = resource === "contacts" ? rows.filter((r) => isLocked(r, "work_email")).length : 0;
|
|
95
|
+
const lockedPhones = resource === "contacts" ? rows.filter((r) => isLocked(r, "direct_phone") || isLocked(r, "mobile_phone")).length : 0;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className={`flex flex-col gap-3 ${className ?? ""}`}>
|
|
99
|
+
{search.error ? <ApiError error={search.error} title="Search failed" onRetry={() => void search.refetch()} /> : null}
|
|
100
|
+
{lockedEmails + lockedPhones > 0 ? (
|
|
101
|
+
<p role="note" className="rounded-md border bg-muted/40 px-3 py-2 text-sm">
|
|
102
|
+
{lockedEmails} emails and {lockedPhones} phones on this page are locked. Save the results to a list to unlock them.
|
|
103
|
+
</p>
|
|
104
|
+
) : null}
|
|
105
|
+
{info?.notes?.map((n) => <p key={n} className="text-xs text-muted-foreground">{n}</p>)}
|
|
106
|
+
|
|
107
|
+
<div className="flex items-center justify-between text-sm">
|
|
108
|
+
<span className="text-muted-foreground">{search.loading && !search.data ? "Searching..." : `${available.toLocaleString()}${lowerBound ? "+" : ""} ${resource} match`}</span>
|
|
109
|
+
<Button size="sm" disabled={rows.length === 0} onClick={() => setSaving(true)}>Save results to a list</Button>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div className="rounded-md border">
|
|
113
|
+
<Table>
|
|
114
|
+
<TableHeader>
|
|
115
|
+
{resource === "contacts"
|
|
116
|
+
? <TableRow><TableHead>Name</TableHead><TableHead>Title</TableHead><TableHead>Company</TableHead><TableHead>Email</TableHead><TableHead>Phone</TableHead><TableHead>Location</TableHead></TableRow>
|
|
117
|
+
: <TableRow><TableHead>Company</TableHead><TableHead>Domain</TableHead><TableHead>Industry</TableHead><TableHead>Employees</TableHead><TableHead>Location</TableHead></TableRow>}
|
|
118
|
+
</TableHeader>
|
|
119
|
+
<TableBody>
|
|
120
|
+
{search.loading && rows.length === 0
|
|
121
|
+
? Array.from({ length: 5 }, (_, i) => <TableRow key={`sk${i}`}><TableCell colSpan={resource === "contacts" ? 6 : 5}><Skeleton className="h-5 w-full" /></TableCell></TableRow>)
|
|
122
|
+
: rows.map((r, i) => resource === "contacts" ? (
|
|
123
|
+
<TableRow key={i} data-testid="prospect-row">
|
|
124
|
+
<TableCell className="font-medium">{[r.first_name, r.last_name].filter(Boolean).join(" ") || "-"}</TableCell>
|
|
125
|
+
<TableCell>{r.job_title ?? "-"}</TableCell>
|
|
126
|
+
<TableCell>{r.company_name ?? "-"}</TableCell>
|
|
127
|
+
<TableCell><Cell row={r} field="work_email" /></TableCell>
|
|
128
|
+
<TableCell>{isLocked(r, "direct_phone") || isLocked(r, "mobile_phone") ? <Locked /> : r.direct_phone || r.mobile_phone || <span className="text-muted-foreground">-</span>}</TableCell>
|
|
129
|
+
<TableCell>{[r.city, r.state, r.country].filter(Boolean).join(", ") || "-"}</TableCell>
|
|
130
|
+
</TableRow>
|
|
131
|
+
) : (
|
|
132
|
+
<TableRow key={i} data-testid="prospect-row">
|
|
133
|
+
<TableCell className="font-medium">{r.name ?? "-"}</TableCell><TableCell>{r.domain ?? "-"}</TableCell>
|
|
134
|
+
<TableCell>{r.industry ?? "-"}</TableCell><TableCell>{r.employee_count ?? "-"}</TableCell>
|
|
135
|
+
<TableCell>{[r.city, r.state, r.country].filter(Boolean).join(", ") || "-"}</TableCell>
|
|
136
|
+
</TableRow>
|
|
137
|
+
))}
|
|
138
|
+
</TableBody>
|
|
139
|
+
</Table>
|
|
140
|
+
{!search.loading && !search.error && rows.length === 0 ? <EmptyState title="No matches" description="Loosen a filter or pick values from the suggestions." /> : null}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div className="flex items-center justify-end gap-2 text-sm text-muted-foreground">
|
|
144
|
+
<Button variant="outline" size="sm" disabled={page <= 1 || search.loading} onClick={() => setState({ ...state, key, page: page - 1 })}>Previous</Button>
|
|
145
|
+
<span>Page {page}</span>
|
|
146
|
+
<Button variant="outline" size="sm" disabled={!info?.has_next || page >= maxPage || search.loading} onClick={() => setState({ ...state, key, page: page + 1 })}>Next</Button>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Mounted only while open so its size field is seeded from the match count at that moment. */}
|
|
150
|
+
{saving ? <SaveToListDialog open onOpenChange={setSaving} resource={resource} filters={applied} estimatedTotal={available}
|
|
151
|
+
defaultTitle={`Search ${new Date().toISOString().slice(0, 10)}`} creditsPerRecord={creditsPerRecord} onSaved={onSaved} /> : null}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Pass `filters` to render only the results. Without it this is a self-contained prospecting page (filters, saved
|
|
158
|
+
* searches and results), so it can be mounted as a route with no props.
|
|
159
|
+
*/
|
|
160
|
+
export function ProspectResults(props: ProspectResultsProps) {
|
|
161
|
+
const { filters, ...rest } = props;
|
|
162
|
+
const [draft, setDraft] = useState<SearchCondition[]>([]);
|
|
163
|
+
const [applied, setApplied] = useState<SearchCondition[]>([]);
|
|
164
|
+
if (filters) return <ResultsTable {...rest} filters={filters} />;
|
|
165
|
+
const resource = props.resource ?? "contacts";
|
|
166
|
+
return (
|
|
167
|
+
<div className={`grid gap-4 lg:grid-cols-[22rem_1fr] ${props.className ?? ""}`}>
|
|
168
|
+
<div className="flex flex-col gap-4">
|
|
169
|
+
<FilterBuilder resource={resource} value={draft} onChange={setDraft} onSearch={setApplied} />
|
|
170
|
+
<SavedSearches resource={resource} current={draft} onLoad={(f) => { setDraft(f); setApplied(completeFilters(f)); }} />
|
|
171
|
+
</div>
|
|
172
|
+
<ResultsTable {...rest} className={undefined} filters={applied} />
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|