@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,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title List a stage's locked contacts, then unlock the ones the person chose
|
|
3
|
+
* @when The customer needs the real emails and phones of contacts in a stage. Details arrive masked as "***" until a
|
|
4
|
+
* contact is unlocked, and unlocking charges credits per NEWLY unlocked contact (billable tier). Two steps on purpose:
|
|
5
|
+
* `listLocked` is free; call `unlock` only with the ids the person agreed to pay for. `lifecycle_id` is the stage ENTRY id.
|
|
6
|
+
*/
|
|
7
|
+
import { g8 } from "@graph8/sdk";
|
|
8
|
+
|
|
9
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
10
|
+
|
|
11
|
+
type Contact = { id: number; email?: string | null };
|
|
12
|
+
|
|
13
|
+
export async function listLocked(stageSlug: string, limit = 50): Promise<{ stageId: string; lockedIds: number[] }> {
|
|
14
|
+
const stages = (await g8.api.revenueLifecycle.listLifecycleStages({})).data;
|
|
15
|
+
const stage = stages.find((s) => s.slug === stageSlug);
|
|
16
|
+
if (!stage) throw new Error(`No lifecycle stage "${stageSlug}"`);
|
|
17
|
+
const res = (await g8.api.revenueRecords.listLifecycleContacts({ query: { lifecycle_id: stage.id, limit } })) as { data?: { contacts?: Contact[] } };
|
|
18
|
+
return { stageId: stage.id, lockedIds: (res.data?.contacts ?? []).filter((c) => c.email === "***").map((c) => c.id) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function unlock(stageId: string, contactIds: number[]): Promise<number | undefined> {
|
|
22
|
+
if (contactIds.length === 0) return 0;
|
|
23
|
+
const out = (await g8.api.revenueRecords.unlockLifecycleContacts({ body: { lifecycle_id: stageId, contact_ids: contactIds } })) as { data?: { credits_charged?: number } };
|
|
24
|
+
return out.data?.credits_charged;
|
|
25
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# lists: lists and suppressions
|
|
2
|
+
|
|
3
|
+
Lists are how everything downstream (sequences, campaigns, newsletters, dialer queues) picks its audience, and
|
|
4
|
+
suppressions are the do-not-contact guard those same flows obey. This block gives you the screens for both.
|
|
5
|
+
It builds on the `crm` block (the members panel is a `ContactTable`).
|
|
6
|
+
|
|
7
|
+
Install: `npx @graph8/sdk add lists` (installs `foundation` and `crm` first).
|
|
8
|
+
|
|
9
|
+
## What you get
|
|
10
|
+
|
|
11
|
+
| Component | Use it for | Embed props |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `ListTable` | All lists: counts, status, create / rename / delete, members side panel with remove-from-list | `onOpen`, `allowCreate`, `pageSize`, `refreshKey` |
|
|
14
|
+
| `ListPicker` | Choose a destination list (search, or create one inline) | `value`, `onChange`, `type`, `allowCreate`, `suggestedName` |
|
|
15
|
+
| `AddToListButton` | "Add to list" for a selection of contacts | `contactIds`, `listId`, `onAdded`, `label` |
|
|
16
|
+
| `SuppressionManager` | The do-not-contact ledger: filter, suppress by id, check, reinstate | `onOpenContact`, `pageSize` |
|
|
17
|
+
|
|
18
|
+
## The flow, with the ids that pass between steps
|
|
19
|
+
|
|
20
|
+
1. **Index**: `GET /lists?page&limit&search` returns lists with `id`, `title`, `type`, `total`, `status`,
|
|
21
|
+
`status_reason` (why a list failed or is stuck), `is_dynamic`.
|
|
22
|
+
2. **Create**: `POST /lists { title, type: "contacts" | "companies" }` returns the new list `id`.
|
|
23
|
+
3. **Fill**: `POST /lists/{list_id}/contacts { contact_ids }` (the ids come from a `ContactTable` selection or a
|
|
24
|
+
contact read). The answer says `added`, `already_member`, `not_found`. A `409` with
|
|
25
|
+
`detail.code === "CONFLICT_REVIEW_REQUIRED"` means some contacts belong to other reps: `AddToListButton`
|
|
26
|
+
shows `ready_count / warned / blocked` and retries with `conflict_resolution: "skip_all" | "add_all"`.
|
|
27
|
+
4. **Members**: `ContactTable listId={id}` (or `GET /contacts?list_id=`). Removing uses
|
|
28
|
+
`DELETE /lists/{list_id}/contacts { contact_ids }`; contacts are never deleted by it.
|
|
29
|
+
5. **Suppress**: `POST /contacts/suppressions/bulk-add { contact_ids, channel, reason }`; read back with
|
|
30
|
+
`GET /contacts/suppressions`. The ledger has ids only (no names or emails): open the contact for details.
|
|
31
|
+
6. **Reinstate**: `POST /contacts/suppressions/reinstate { contact_ids }`. Contacts who unsubscribed themselves
|
|
32
|
+
can be refused; the answer lists them in `skipped`.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
"use client";
|
|
36
|
+
import { ContactTable } from "@/components/g8/crm/contact-table";
|
|
37
|
+
import { AddToListButton } from "@/components/g8/lists/add-to-list-button";
|
|
38
|
+
|
|
39
|
+
export function Prospects() {
|
|
40
|
+
return (
|
|
41
|
+
<ContactTable selectable customFields
|
|
42
|
+
renderBulkActions={({ ids, clear }) => <AddToListButton contactIds={ids} onAdded={clear} />} />
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
(`add` installs under `components/g8/lists/`, or `src/components/g8/lists/`; adjust the alias.)
|
|
48
|
+
|
|
49
|
+
## Tiers and confirmations
|
|
50
|
+
|
|
51
|
+
- Create, rename and add/remove membership are writes: the dialog you open is the confirmation.
|
|
52
|
+
- `ListTable`: deleting a list and removing members are `destructive` and go through `ConfirmSpendDialog`.
|
|
53
|
+
Neither deletes contacts.
|
|
54
|
+
- `SuppressionManager`: suppressing only reduces outreach, so it runs directly. Reinstating (write tier) lets
|
|
55
|
+
sequences and campaigns contact people again, so it is confirmed with `ConfirmSpendDialog` as `external`.
|
|
56
|
+
|
|
57
|
+
## Prerequisites
|
|
58
|
+
|
|
59
|
+
- `crm-installed` (app): `components/g8/crm/contact-table.tsx` exists (the CLI installs it).
|
|
60
|
+
- `proxy-allowlist` (app): `allowedPrefixes` includes `lists` and `contacts`.
|
|
61
|
+
- `suppression-permission` (api): the key has `contacts:write`, `lists:write`, `lists:delete`.
|
|
62
|
+
|
|
63
|
+
## Defaults the agent can use without asking
|
|
64
|
+
|
|
65
|
+
| Decision | Default |
|
|
66
|
+
|---|---|
|
|
67
|
+
| Type of a new list | `contacts` |
|
|
68
|
+
| Channel when suppressing | `all` |
|
|
69
|
+
| Rep conflicts when adding | show the counts and let the person choose |
|
|
70
|
+
|
|
71
|
+
Ask the person: who may reinstate suppressed contacts (decide it in the proxy's `authorize`).
|
|
72
|
+
|
|
73
|
+
## Recipes
|
|
74
|
+
|
|
75
|
+
| File | Does |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `recipes/create-list-and-add-contacts.ts` | Create a list, add contacts, handle the 409 review, read and remove members |
|
|
78
|
+
| `recipes/suppress-and-reinstate.ts` | Bulk suppress, check, list by channel, reinstate (after approval) |
|
|
79
|
+
| `recipes/refresh-and-check-lists.ts` | Refresh a dynamic list; read-only integrity check |
|
|
80
|
+
|
|
81
|
+
## Operations
|
|
82
|
+
|
|
83
|
+
| Operation | Method and path | Tier | Used by |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| listLists | GET /lists | read | ListTable, ListPicker, AddToListButton |
|
|
86
|
+
| createList | POST /lists | write | ListTable, ListPicker, AddToListButton |
|
|
87
|
+
| updateList | PATCH /lists/{list_id} | write | ListTable |
|
|
88
|
+
| deleteList | DELETE /lists/{list_id} | destructive | ListTable |
|
|
89
|
+
| addContactsToList | POST /lists/{list_id}/contacts | write | AddToListButton |
|
|
90
|
+
| removeContactsFromList | DELETE /lists/{list_id}/contacts | destructive | ListTable |
|
|
91
|
+
| listContacts | GET /contacts?list_id= | read | ListTable (members panel) |
|
|
92
|
+
| listSuppressions | GET /contacts/suppressions | read | SuppressionManager |
|
|
93
|
+
| bulkAddSuppressions | POST /contacts/suppressions/bulk-add | write | SuppressionManager |
|
|
94
|
+
| checkContactSuppression | GET /contacts/{contact_id}/suppression | read | SuppressionManager |
|
|
95
|
+
| reinstateSuppressions | POST /contacts/suppressions/reinstate | write | SuppressionManager |
|
|
96
|
+
| refreshDynamicList, checkListIntegrity | POST /lists/{id}/refresh, /lists/{id}/integrity-check | write, read | recipes |
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../block.schema.json",
|
|
3
|
+
"id": "lists",
|
|
4
|
+
"title": "Lists & suppressions",
|
|
5
|
+
"summary": "List table with members panel, list picker, add-to-list button and a do-not-contact suppression manager.",
|
|
6
|
+
"features": [
|
|
7
|
+
"list index with member counts, status, create, rename and delete",
|
|
8
|
+
"members panel built on the CRM ContactTable with remove-from-list",
|
|
9
|
+
"ListPicker (search or create) and AddToListButton for any contact selection",
|
|
10
|
+
"conflict review when contacts belong to other reps",
|
|
11
|
+
"suppression ledger by channel and reason, bulk suppress, check, reinstate"
|
|
12
|
+
],
|
|
13
|
+
"dependsOn": [
|
|
14
|
+
"crm"
|
|
15
|
+
],
|
|
16
|
+
"operations": [
|
|
17
|
+
"add_contacts_to_list_lists__list_id__contacts_post",
|
|
18
|
+
"bulk_add_suppressions_contacts_suppressions_bulk_add_post",
|
|
19
|
+
"check_contact_suppression_contacts__contact_id__suppression_get",
|
|
20
|
+
"check_list_integrity_lists__list_id__integrity_check_post",
|
|
21
|
+
"create_list_lists_post",
|
|
22
|
+
"delete_list_lists__list_id__delete",
|
|
23
|
+
"list_contact_fields_fields_get",
|
|
24
|
+
"list_contacts_contacts_get",
|
|
25
|
+
"list_lists_lists_get",
|
|
26
|
+
"list_suppressions_contacts_suppressions_get",
|
|
27
|
+
"refresh_dynamic_list_lists__audience_id__refresh_post",
|
|
28
|
+
"reinstate_suppressions_contacts_suppressions_reinstate_post",
|
|
29
|
+
"remove_contacts_from_list_lists__list_id__contacts_delete",
|
|
30
|
+
"update_list_lists__list_id__patch"
|
|
31
|
+
],
|
|
32
|
+
"components": [
|
|
33
|
+
{
|
|
34
|
+
"name": "ListTable",
|
|
35
|
+
"file": "components/list-table.tsx",
|
|
36
|
+
"purpose": "All lists with counts and status; create, rename, delete; side panel with the list's members (CRM ContactTable) and remove-from-list.",
|
|
37
|
+
"operations": [
|
|
38
|
+
"list_lists_lists_get",
|
|
39
|
+
"create_list_lists_post",
|
|
40
|
+
"update_list_lists__list_id__patch",
|
|
41
|
+
"delete_list_lists__list_id__delete",
|
|
42
|
+
"remove_contacts_from_list_lists__list_id__contacts_delete",
|
|
43
|
+
"list_contacts_contacts_get",
|
|
44
|
+
"list_contact_fields_fields_get"
|
|
45
|
+
],
|
|
46
|
+
"tiers": [
|
|
47
|
+
"read",
|
|
48
|
+
"write",
|
|
49
|
+
"destructive"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "ListPicker",
|
|
54
|
+
"file": "components/list-picker.tsx",
|
|
55
|
+
"purpose": "Search lists and pick one, or create a new list inline. The destination chooser for every save-to-list flow.",
|
|
56
|
+
"operations": [
|
|
57
|
+
"list_lists_lists_get",
|
|
58
|
+
"create_list_lists_post"
|
|
59
|
+
],
|
|
60
|
+
"tiers": [
|
|
61
|
+
"read",
|
|
62
|
+
"write"
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "AddToListButton",
|
|
67
|
+
"file": "components/add-to-list-button.tsx",
|
|
68
|
+
"purpose": "Add a selection of contacts to a list from a dialog; reports added, already-member and not-found, and handles the rep-conflict review.",
|
|
69
|
+
"operations": [
|
|
70
|
+
"list_lists_lists_get",
|
|
71
|
+
"create_list_lists_post",
|
|
72
|
+
"add_contacts_to_list_lists__list_id__contacts_post"
|
|
73
|
+
],
|
|
74
|
+
"tiers": [
|
|
75
|
+
"read",
|
|
76
|
+
"write"
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"name": "SuppressionManager",
|
|
81
|
+
"file": "components/suppression-manager.tsx",
|
|
82
|
+
"purpose": "The do-not-contact ledger: filter by channel and reason, suppress by id, check a contact, reinstate (confirmed).",
|
|
83
|
+
"operations": [
|
|
84
|
+
"list_suppressions_contacts_suppressions_get",
|
|
85
|
+
"bulk_add_suppressions_contacts_suppressions_bulk_add_post",
|
|
86
|
+
"check_contact_suppression_contacts__contact_id__suppression_get",
|
|
87
|
+
"reinstate_suppressions_contacts_suppressions_reinstate_post"
|
|
88
|
+
],
|
|
89
|
+
"tiers": [
|
|
90
|
+
"read",
|
|
91
|
+
"write"
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"hooks": [],
|
|
96
|
+
"recipes": [
|
|
97
|
+
{
|
|
98
|
+
"file": "recipes/create-list-and-add-contacts.ts",
|
|
99
|
+
"title": "Create a list, add contacts to it and read the members",
|
|
100
|
+
"when": "Grouping people for a sequence, campaign or export. A list's `type` is `contacts` or `companies`. The list id comes from the create response; contact ids come from a contact read or search, never a guess. Adding is idempotent: contacts already in the list are reported in `already_member`, ids that do not exist in `not_found`. Contacts owned by other reps can answer 409 CONFLICT_REVIEW_REQUIRED: repeat the call with `conflict_resolution` once a person decided."
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"file": "recipes/suppress-and-reinstate.ts",
|
|
104
|
+
"title": "Suppress contacts and reinstate them later",
|
|
105
|
+
"when": "Honouring an opt-out, a bounce or a \"do not call\" request. Suppression is respected by every sequence, campaign and newsletter. Reinstating lets outreach resume, so ask a human first. The ledger holds contact ids only; resolve names with `g8.contacts.get`."
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"file": "recipes/refresh-and-check-lists.ts",
|
|
109
|
+
"title": "Refresh a dynamic list and check a list's health",
|
|
110
|
+
"when": "A list is built from filters (dynamic) and must be re-evaluated, or a list looks wrong (stuck importing, member count off). `refresh` re-runs the filters; the integrity check is read-only and reports problems without changing anything."
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"prerequisites": [
|
|
114
|
+
{
|
|
115
|
+
"id": "crm-installed",
|
|
116
|
+
"where": "app",
|
|
117
|
+
"description": "The crm block is installed (the CLI does it): ListTable embeds its ContactTable.",
|
|
118
|
+
"verify": "components/g8/crm/contact-table.tsx exists."
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": "proxy-allowlist",
|
|
122
|
+
"where": "app",
|
|
123
|
+
"description": "allowedPrefixes in the /api/g8 route includes `lists` and `contacts` (suppressions live under /contacts/suppressions).",
|
|
124
|
+
"verify": "GET /api/g8/lists?limit=1 and GET /api/g8/contacts/suppressions?limit=1 return 200 when signed in."
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"id": "suppression-permission",
|
|
128
|
+
"where": "api",
|
|
129
|
+
"description": "The API key can write contacts (suppress and reinstate need contacts:write) and lists (lists:write, lists:delete).",
|
|
130
|
+
"verify": "g8.api.me.describeCurrentKey() lists contacts:write, lists:write and lists:delete."
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
"scopes": [
|
|
134
|
+
"contacts:read",
|
|
135
|
+
"contacts:write",
|
|
136
|
+
"fields:read",
|
|
137
|
+
"lists:delete",
|
|
138
|
+
"lists:read",
|
|
139
|
+
"lists:write"
|
|
140
|
+
],
|
|
141
|
+
"defaults": [
|
|
142
|
+
{
|
|
143
|
+
"decision": "Type of a newly created list",
|
|
144
|
+
"default": "contacts",
|
|
145
|
+
"askHuman": false
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"decision": "Channel when suppressing",
|
|
149
|
+
"default": "all (every channel)",
|
|
150
|
+
"askHuman": false
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"decision": "Conflicting contacts (owned by other reps) when adding to a list",
|
|
154
|
+
"default": "Show the counts and let the person pick 'ready only' or 'ready and flagged'; never auto-pick",
|
|
155
|
+
"askHuman": false
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"decision": "Who may reinstate suppressed contacts",
|
|
159
|
+
"default": "Anyone passing the proxy's authorize; reinstating is confirmed in the UI",
|
|
160
|
+
"askHuman": true
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
"peerDependencies": {
|
|
164
|
+
"react": ">=18"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AddToListButton: "Add to list" for any selection of contacts. Opens a dialog with
|
|
5
|
+
* ListPicker, adds the contacts, and reports what happened (added, already in the
|
|
6
|
+
* list, not found). Drop it into ContactTable's `renderBulkActions` or ContactDetail's
|
|
7
|
+
* `renderActions`.
|
|
8
|
+
*
|
|
9
|
+
* SDK operations, in order:
|
|
10
|
+
* 1. listLists / createList via ListPicker (read, write)
|
|
11
|
+
* 2. addContactsToList POST /lists/{list_id}/contacts { contact_ids } (write)
|
|
12
|
+
* A 409 CONFLICT_REVIEW_REQUIRED (contacts owned by other reps) is shown with its counts and the
|
|
13
|
+
* choice "add ready only" or "add ready and flagged"; the call is repeated with `conflict_resolution`.
|
|
14
|
+
*
|
|
15
|
+
* Adding is a write the person just chose in the dialog, so it needs no extra confirmation. Saving a
|
|
16
|
+
* contact to a list is also how locked ("***") search results become real records.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@graph8/sdk/ui";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
23
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
24
|
+
import { ListPicker } from "./list-picker";
|
|
25
|
+
import type { ListSummary } from "./list-picker";
|
|
26
|
+
|
|
27
|
+
export interface AddToListResult {
|
|
28
|
+
listId: number;
|
|
29
|
+
added: number;
|
|
30
|
+
already_member: number[];
|
|
31
|
+
not_found: number[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AddToListButtonProps {
|
|
35
|
+
contactIds: number[];
|
|
36
|
+
/** Skip the picker and add straight into this list. */
|
|
37
|
+
listId?: number;
|
|
38
|
+
label?: string;
|
|
39
|
+
variant?: "default" | "outline" | "secondary" | "ghost";
|
|
40
|
+
size?: "default" | "sm";
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
onAdded?: (result: AddToListResult) => void;
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ConflictDetail { code?: string; ready_count?: number; warned?: unknown[]; blocked?: unknown[]; message?: string }
|
|
47
|
+
type Resolution = "add_all" | "skip_all";
|
|
48
|
+
|
|
49
|
+
function conflictOf(error: unknown): ConflictDetail | null {
|
|
50
|
+
const e = error as { status?: number; detail?: ConflictDetail } | null;
|
|
51
|
+
return e?.status === 409 && e.detail && typeof e.detail === "object" && e.detail.code === "CONFLICT_REVIEW_REQUIRED" ? e.detail : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function AddToListButton({ contactIds, listId, label = "Add to list", variant = "outline", size = "sm", disabled, onAdded, className }: AddToListButtonProps) {
|
|
55
|
+
const [open, setOpen] = useState(false);
|
|
56
|
+
const [target, setTarget] = useState<ListSummary | null>(null);
|
|
57
|
+
const [result, setResult] = useState<AddToListResult | null>(null);
|
|
58
|
+
const destination = listId ?? target?.id;
|
|
59
|
+
|
|
60
|
+
const add = useG8Mutation(async (resolution?: Resolution) => {
|
|
61
|
+
const res = await g8Post<{ added: number; already_member?: number[]; not_found?: number[] }>(`/lists/${destination}/contacts`, {
|
|
62
|
+
contact_ids: contactIds,
|
|
63
|
+
...(resolution ? { conflict_resolution: resolution } : {}),
|
|
64
|
+
});
|
|
65
|
+
return { listId: destination as number, added: res.data.added, already_member: res.data.already_member ?? [], not_found: res.data.not_found ?? [] };
|
|
66
|
+
}, { onSuccess: (r) => { setResult(r); onAdded?.(r); } });
|
|
67
|
+
|
|
68
|
+
const conflict = conflictOf(add.error);
|
|
69
|
+
const close = () => { setOpen(false); setResult(null); setTarget(null); add.reset(); };
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<>
|
|
73
|
+
<Button className={className} variant={variant} size={size} disabled={disabled || contactIds.length === 0} onClick={() => setOpen(true)}>{label}</Button>
|
|
74
|
+
<Dialog open={open} onOpenChange={(o) => (o ? setOpen(true) : close())}>
|
|
75
|
+
<DialogContent>
|
|
76
|
+
<DialogHeader>
|
|
77
|
+
<DialogTitle>Add {contactIds.length.toLocaleString()} contact{contactIds.length === 1 ? "" : "s"} to a list</DialogTitle>
|
|
78
|
+
<DialogDescription>Contacts already in the list are skipped.</DialogDescription>
|
|
79
|
+
</DialogHeader>
|
|
80
|
+
|
|
81
|
+
{result ? (
|
|
82
|
+
<div role="status" className="text-sm">
|
|
83
|
+
<p className="font-medium">Added {result.added.toLocaleString()} to the list.</p>
|
|
84
|
+
{result.already_member.length ? <p className="text-muted-foreground">{result.already_member.length} were already in it.</p> : null}
|
|
85
|
+
{result.not_found.length ? <p className="text-muted-foreground">{result.not_found.length} no longer exist.</p> : null}
|
|
86
|
+
</div>
|
|
87
|
+
) : (
|
|
88
|
+
<>
|
|
89
|
+
{listId == null ? <ListPicker value={target?.id} onChange={(l) => { setTarget(l); add.reset(); }} /> : null}
|
|
90
|
+
{conflict ? (
|
|
91
|
+
<div role="alert" className="rounded-md border border-amber-300 bg-amber-50 p-3 text-sm">
|
|
92
|
+
<p>{conflict.message ?? "Some contacts belong to other reps."}</p>
|
|
93
|
+
<p className="mt-1">{conflict.ready_count ?? 0} ready, {conflict.warned?.length ?? 0} flagged, {conflict.blocked?.length ?? 0} blocked (never added).</p>
|
|
94
|
+
<div className="mt-2 flex gap-2">
|
|
95
|
+
<Button size="sm" variant="outline" loading={add.loading} onClick={() => void add.mutate("skip_all")}>Add ready only</Button>
|
|
96
|
+
<Button size="sm" loading={add.loading} onClick={() => void add.mutate("add_all")}>Add ready and flagged</Button>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
) : add.error ? <ApiError error={add.error} title="Could not add the contacts" /> : null}
|
|
100
|
+
</>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<DialogFooter>
|
|
104
|
+
<Button variant="ghost" onClick={close}>{result ? "Done" : "Cancel"}</Button>
|
|
105
|
+
{!result ? <Button loading={add.loading && !conflict} disabled={destination == null} onClick={() => void add.mutate(undefined)}>Add to list</Button> : null}
|
|
106
|
+
</DialogFooter>
|
|
107
|
+
</DialogContent>
|
|
108
|
+
</Dialog>
|
|
109
|
+
</>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ListPicker: search lists by name, pick one, or create a new one on the spot.
|
|
5
|
+
* The destination chooser for every "save to a list" flow (AddToListButton,
|
|
6
|
+
* search results, import, sequence enrollment).
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. listLists GET /lists?search&limit (read) lists of the wanted `type`
|
|
10
|
+
* 2. createList POST /lists (write) only when `allowCreate`; the new list is selected at once
|
|
11
|
+
*
|
|
12
|
+
* Creating a list is a write that is easy to undo, so it does not need ConfirmSpendDialog.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useEffect, useState } from "react";
|
|
16
|
+
import { Button, Input } from "@graph8/sdk/ui";
|
|
17
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
18
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
19
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
20
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
21
|
+
|
|
22
|
+
export interface ListSummary {
|
|
23
|
+
id: number;
|
|
24
|
+
title: string;
|
|
25
|
+
type?: string | null;
|
|
26
|
+
description?: string | null;
|
|
27
|
+
status?: string | null;
|
|
28
|
+
status_reason?: string | null;
|
|
29
|
+
status_detail?: string | null;
|
|
30
|
+
total?: number;
|
|
31
|
+
contact_count?: number | null;
|
|
32
|
+
company_count?: number | null;
|
|
33
|
+
is_dynamic?: boolean;
|
|
34
|
+
created_at?: string | null;
|
|
35
|
+
updated_at?: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ListPickerProps {
|
|
39
|
+
/** Selected list id. */
|
|
40
|
+
value?: number | null;
|
|
41
|
+
onChange: (list: ListSummary) => void;
|
|
42
|
+
/** Only lists of this type. Default "contacts". */
|
|
43
|
+
type?: "contacts" | "companies";
|
|
44
|
+
allowCreate?: boolean;
|
|
45
|
+
/** Pre-fill the new list name (for example "Search: VPs in fintech"). */
|
|
46
|
+
suggestedName?: string;
|
|
47
|
+
className?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function useDebounced<T>(value: T, ms = 250): T {
|
|
51
|
+
const [v, setV] = useState(value);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const t = setTimeout(() => setV(value), ms);
|
|
54
|
+
return () => clearTimeout(t);
|
|
55
|
+
}, [value, ms]);
|
|
56
|
+
return v;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function ListPicker({ value, onChange, type = "contacts", allowCreate = true, suggestedName, className }: ListPickerProps) {
|
|
60
|
+
const [search, setSearch] = useState("");
|
|
61
|
+
const [newName, setNewName] = useState(suggestedName ?? "");
|
|
62
|
+
const q = useDebounced(search.trim());
|
|
63
|
+
const lists = useG8Query((signal) => g8Get<ListSummary[]>("/lists", { search: q || undefined, limit: 50 }, { signal }), [q]);
|
|
64
|
+
const create = useG8Mutation((title: string) => g8Post<ListSummary>("/lists", { title, type }), {
|
|
65
|
+
onSuccess: (res) => { setNewName(""); void lists.refetch(); onChange({ type, ...res.data, title: res.data.title ?? newName }); },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const rows = ((lists.data?.data ?? []) as ListSummary[]).filter((l) => !l.type || l.type === type);
|
|
69
|
+
return (
|
|
70
|
+
<div className={`flex flex-col gap-2 ${className ?? ""}`}>
|
|
71
|
+
<Input aria-label="Search lists" placeholder="Search lists" value={search} onChange={(e) => setSearch(e.target.value)} />
|
|
72
|
+
{lists.error ? <ApiError error={lists.error} title="Could not load lists" onRetry={() => void lists.refetch()} /> : null}
|
|
73
|
+
<ul role="listbox" aria-label="Lists" className="max-h-56 overflow-y-auto rounded-md border">
|
|
74
|
+
{lists.loading && rows.length === 0 ? <li className="p-3 text-sm text-muted-foreground">Loading lists...</li> : null}
|
|
75
|
+
{!lists.loading && !lists.error && rows.length === 0 ? <li className="p-3 text-sm text-muted-foreground">{q ? "No list matches." : "No lists yet."}</li> : null}
|
|
76
|
+
{rows.map((l) => (
|
|
77
|
+
<li key={l.id} role="option" aria-selected={l.id === value}>
|
|
78
|
+
<button type="button" className={`flex w-full items-center justify-between px-3 py-2 text-left text-sm hover:bg-accent ${l.id === value ? "bg-accent font-medium" : ""}`} onClick={() => onChange(l)}>
|
|
79
|
+
<span className="truncate">{l.title}</span>
|
|
80
|
+
<span className="text-xs text-muted-foreground">{(l.total ?? 0).toLocaleString()}</span>
|
|
81
|
+
</button>
|
|
82
|
+
</li>
|
|
83
|
+
))}
|
|
84
|
+
</ul>
|
|
85
|
+
{allowCreate ? (
|
|
86
|
+
<form className="flex items-center gap-2" onSubmit={(e) => { e.preventDefault(); if (newName.trim()) void create.mutate(newName.trim()); }}>
|
|
87
|
+
<Input aria-label="New list name" placeholder="Or create a new list" value={newName} onChange={(e) => setNewName(e.target.value)} />
|
|
88
|
+
<Button type="submit" variant="outline" size="sm" loading={create.loading} disabled={!newName.trim()}>Create</Button>
|
|
89
|
+
</form>
|
|
90
|
+
) : null}
|
|
91
|
+
{create.error ? <ApiError error={create.error} title="Could not create the list" /> : null}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ListTable: all lists with member counts and status, create / rename / delete, and a
|
|
5
|
+
* side panel with the list's members (a ContactTable) where selected contacts can be
|
|
6
|
+
* removed from the list.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. listLists GET /lists?page&limit&search (read)
|
|
10
|
+
* 2. createList POST /lists (write)
|
|
11
|
+
* 3. updateList PATCH /lists/{list_id} { title, description } (write)
|
|
12
|
+
* 4. deleteList DELETE /lists/{list_id} (destructive, ConfirmSpendDialog)
|
|
13
|
+
* 5. listContacts GET /contacts?list_id=... via ContactTable (read) members panel
|
|
14
|
+
* removeContactsFromList DELETE /lists/{list_id}/contacts { contact_ids } (destructive, ConfirmSpendDialog)
|
|
15
|
+
*
|
|
16
|
+
* Deleting a list never deletes its contacts; removing members from a list does not either.
|
|
17
|
+
* `status_reason` explains a list that failed to build or is stuck importing.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useState } from "react";
|
|
21
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
22
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
23
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
24
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
25
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
|
+
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
27
|
+
import { ContactTable } from "@/g8/crm/components/contact-table";
|
|
28
|
+
import { useDebouncedValue } from "@/g8/crm/hooks/use-debounced";
|
|
29
|
+
import { usePageQuery } from "@/g8/crm/hooks/use-page-query";
|
|
30
|
+
import type { ListSummary } from "./list-picker";
|
|
31
|
+
|
|
32
|
+
export interface ListTableProps {
|
|
33
|
+
pageSize?: number;
|
|
34
|
+
/** Called when a list row is clicked. Without it, contact lists open the members panel. */
|
|
35
|
+
onOpen?: (list: ListSummary) => void;
|
|
36
|
+
allowCreate?: boolean;
|
|
37
|
+
refreshKey?: number;
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function count(l: ListSummary): number {
|
|
42
|
+
return l.contact_count ?? l.company_count ?? l.total ?? 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const STATUS_STYLE: Record<string, string> = { failed: "text-destructive", processing: "text-amber-600" };
|
|
46
|
+
|
|
47
|
+
export function ListTable({ pageSize = 25, onOpen, allowCreate = true, refreshKey, className }: ListTableProps) {
|
|
48
|
+
const [search, setSearch] = useState("");
|
|
49
|
+
const debouncedSearch = useDebouncedValue(search);
|
|
50
|
+
const [editing, setEditing] = useState<{ id?: number; title: string; description: string; type: "contacts" | "companies" } | null>(null);
|
|
51
|
+
const [deleting, setDeleting] = useState<ListSummary | null>(null);
|
|
52
|
+
const [members, setMembers] = useState<ListSummary | null>(null);
|
|
53
|
+
const [removing, setRemoving] = useState<number[] | null>(null);
|
|
54
|
+
const [membersKey, setMembersKey] = useState(0);
|
|
55
|
+
|
|
56
|
+
const lists = usePageQuery<ListSummary>(
|
|
57
|
+
(page, signal) => g8Get<ListSummary[]>("/lists", { page, limit: pageSize, search: debouncedSearch.trim() || undefined }, { signal }),
|
|
58
|
+
[debouncedSearch, refreshKey],
|
|
59
|
+
pageSize,
|
|
60
|
+
);
|
|
61
|
+
const save = useG8Mutation(
|
|
62
|
+
(v: NonNullable<typeof editing>) => (v.id ? g8Patch(`/lists/${v.id}`, { title: v.title, description: v.description }) : g8Post("/lists", { title: v.title, type: v.type, description: v.description || undefined })),
|
|
63
|
+
{ onSuccess: () => { setEditing(null); void lists.refetch(); } },
|
|
64
|
+
);
|
|
65
|
+
const remove = useG8Mutation((id: number) => g8Delete(`/lists/${id}`), { onSuccess: () => { setDeleting(null); void lists.refetch(); } });
|
|
66
|
+
const removeMembers = useG8Mutation((v: { listId: number; ids: number[] }) => g8Delete(`/lists/${v.listId}/contacts`, { body: { contact_ids: v.ids } }), {
|
|
67
|
+
onSuccess: () => { setRemoving(null); setMembersKey((k) => k + 1); void lists.refetch(); },
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const open = (l: ListSummary) => (onOpen ? onOpen(l) : l.type === "companies" ? undefined : setMembers(l));
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className={`flex flex-col gap-3 ${className ?? ""}`}>
|
|
74
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
75
|
+
<Input aria-label="Search lists" placeholder="Search lists" className="w-64" value={search} onChange={(e) => setSearch(e.target.value)} />
|
|
76
|
+
{allowCreate ? <Button className="ml-auto" size="sm" onClick={() => setEditing({ title: "", description: "", type: "contacts" })}>New list</Button> : null}
|
|
77
|
+
</div>
|
|
78
|
+
{lists.error ? <ApiError error={lists.error} title="Could not load lists" onRetry={() => void lists.refetch()} /> : null}
|
|
79
|
+
{remove.error ? <ApiError error={remove.error} title="Could not delete the list" /> : null}
|
|
80
|
+
|
|
81
|
+
<div className="rounded-md border">
|
|
82
|
+
<Table>
|
|
83
|
+
<TableHeader>
|
|
84
|
+
<TableRow><TableHead>Name</TableHead><TableHead>Type</TableHead><TableHead className="text-right">Members</TableHead><TableHead>Status</TableHead><TableHead>Updated</TableHead><TableHead className="w-32" /></TableRow>
|
|
85
|
+
</TableHeader>
|
|
86
|
+
<TableBody>
|
|
87
|
+
{lists.loading && lists.rows.length === 0
|
|
88
|
+
? Array.from({ length: 4 }, (_, i) => <TableRow key={`sk${i}`}><TableCell colSpan={6}><Skeleton className="h-5 w-full" /></TableCell></TableRow>)
|
|
89
|
+
: lists.rows.map((l) => (
|
|
90
|
+
<TableRow key={l.id} data-testid={`list-row-${l.id}`} className="cursor-pointer" onClick={() => open(l)}>
|
|
91
|
+
<TableCell className="font-medium">{l.title}{l.is_dynamic ? <span className="ml-2 text-xs text-muted-foreground">dynamic</span> : null}</TableCell>
|
|
92
|
+
<TableCell className="capitalize">{l.type ?? "-"}</TableCell>
|
|
93
|
+
<TableCell className="text-right">{count(l).toLocaleString()}</TableCell>
|
|
94
|
+
<TableCell className={STATUS_STYLE[l.status ?? ""] ?? ""} title={l.status_detail ?? undefined}>{l.status_reason ? l.status_reason.replace(/_/g, " ") : l.status ?? "-"}</TableCell>
|
|
95
|
+
<TableCell>{l.updated_at ? new Date(l.updated_at).toLocaleDateString() : "-"}</TableCell>
|
|
96
|
+
<TableCell onClick={(e) => e.stopPropagation()}>
|
|
97
|
+
<Button variant="ghost" size="sm" onClick={() => setEditing({ id: l.id, title: l.title, description: l.description ?? "", type: l.type === "companies" ? "companies" : "contacts" })}>Edit</Button>
|
|
98
|
+
<Button variant="ghost" size="sm" onClick={() => setDeleting(l)}>Delete</Button>
|
|
99
|
+
</TableCell>
|
|
100
|
+
</TableRow>
|
|
101
|
+
))}
|
|
102
|
+
</TableBody>
|
|
103
|
+
</Table>
|
|
104
|
+
{!lists.loading && !lists.error && lists.rows.length === 0 ? (
|
|
105
|
+
<EmptyState title={search ? "No list matches" : "No lists yet"} description={search ? "Try another name." : "A list groups contacts for a campaign, sequence or export."}
|
|
106
|
+
action={!search && allowCreate ? <Button size="sm" onClick={() => setEditing({ title: "", description: "", type: "contacts" })}>Create a list</Button> : undefined} />
|
|
107
|
+
) : null}
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
111
|
+
<span>{lists.total.toLocaleString()} lists</span>
|
|
112
|
+
<div className="flex items-center gap-2">
|
|
113
|
+
<Button variant="outline" size="sm" disabled={lists.page <= 1 || lists.loading} onClick={() => lists.setPage(lists.page - 1)}>Previous</Button>
|
|
114
|
+
<span>Page {lists.page} of {lists.pageCount}</span>
|
|
115
|
+
<Button variant="outline" size="sm" disabled={!lists.hasNext || lists.loading} onClick={() => lists.setPage(lists.page + 1)}>Next</Button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<Dialog open={editing != null} onOpenChange={(o) => !o && setEditing(null)}>
|
|
120
|
+
<DialogContent>
|
|
121
|
+
<DialogHeader><DialogTitle>{editing?.id ? "Edit list" : "New list"}</DialogTitle><DialogDescription>Give it a name your team will recognise.</DialogDescription></DialogHeader>
|
|
122
|
+
<div className="flex flex-col gap-3">
|
|
123
|
+
<div className="flex flex-col gap-1"><Label htmlFor="list-title">Name</Label><Input id="list-title" value={editing?.title ?? ""} onChange={(e) => setEditing((v) => v && { ...v, title: e.target.value })} /></div>
|
|
124
|
+
<div className="flex flex-col gap-1"><Label htmlFor="list-desc">Description</Label><Input id="list-desc" value={editing?.description ?? ""} onChange={(e) => setEditing((v) => v && { ...v, description: e.target.value })} /></div>
|
|
125
|
+
{!editing?.id ? (
|
|
126
|
+
<div className="flex flex-col gap-1"><Label htmlFor="list-type">Holds</Label>
|
|
127
|
+
<select id="list-type" className="h-9 rounded-md border bg-background px-2 text-sm" value={editing?.type} onChange={(e) => setEditing((v) => v && { ...v, type: e.target.value as "contacts" | "companies" })}>
|
|
128
|
+
<option value="contacts">Contacts</option><option value="companies">Companies</option>
|
|
129
|
+
</select>
|
|
130
|
+
</div>
|
|
131
|
+
) : null}
|
|
132
|
+
{save.error ? <ApiError error={save.error} title="Could not save the list" /> : null}
|
|
133
|
+
</div>
|
|
134
|
+
<DialogFooter>
|
|
135
|
+
<Button variant="ghost" onClick={() => setEditing(null)}>Cancel</Button>
|
|
136
|
+
<Button loading={save.loading} disabled={!editing?.title.trim()} onClick={() => editing && void save.mutate({ ...editing, title: editing.title.trim() })}>Save</Button>
|
|
137
|
+
</DialogFooter>
|
|
138
|
+
</DialogContent>
|
|
139
|
+
</Dialog>
|
|
140
|
+
|
|
141
|
+
<Sheet open={members != null} onOpenChange={(o) => !o && setMembers(null)}>
|
|
142
|
+
<SheetContent className="w-full sm:max-w-3xl overflow-y-auto p-4">
|
|
143
|
+
<SheetHeader><SheetTitle>{members?.title}</SheetTitle><SheetDescription>{members ? `${count(members).toLocaleString()} members` : ""}</SheetDescription></SheetHeader>
|
|
144
|
+
{members ? (
|
|
145
|
+
<ContactTable listId={members.id} selectable allowDelete={false} pageSize={10} refreshKey={membersKey}
|
|
146
|
+
renderBulkActions={({ ids }) => <Button size="sm" variant="outline" onClick={() => setRemoving(ids)}>Remove from list</Button>} />
|
|
147
|
+
) : null}
|
|
148
|
+
{removeMembers.error ? <ApiError error={removeMembers.error} title="Could not remove them" /> : null}
|
|
149
|
+
{/* Inside the sheet: a modal sheet makes everything outside it inert, so the dialog must live in it. */}
|
|
150
|
+
<ConfirmSpendDialog open={removing != null} tier="destructive" title={`Remove ${removing?.length ?? 0} contacts from "${members?.title ?? "the list"}"`} description="They stay in your CRM; they only leave this list."
|
|
151
|
+
affected={removing?.length} confirmLabel="Remove" confirming={removeMembers.loading}
|
|
152
|
+
onConfirm={() => members && removing && void removeMembers.mutate({ listId: members.id, ids: removing })} onCancel={() => setRemoving(null)} />
|
|
153
|
+
</SheetContent>
|
|
154
|
+
</Sheet>
|
|
155
|
+
|
|
156
|
+
<ConfirmSpendDialog open={deleting != null} tier="destructive" title={`Delete list "${deleting?.title ?? ""}"`} description="The list is removed. Its contacts stay in your CRM."
|
|
157
|
+
affected={deleting ? count(deleting) : undefined} confirming={remove.loading} onConfirm={() => deleting && void remove.mutate(deleting.id)} onCancel={() => setDeleting(null)} />
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|