@graph8/sdk 0.308.1 → 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 +1440 -21
- package/dist/index.d.ts +1440 -21
- package/dist/index.js +42 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +42 -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 +1340 -18
- package/dist/react.d.ts +1340 -18
- package/dist/react.js +41 -2
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +41 -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 +6 -3
- package/docs/api/appObjects.d.ts +1 -1
- package/docs/api/appObjects.md +1 -1
- package/docs/api/by-path.md +29 -0
- package/docs/api/objects.d.ts +72 -5
- package/docs/api/objects.md +84 -7
- package/docs/api/processLists.d.ts +247 -0
- package/docs/api/processLists.md +409 -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,110 @@
|
|
|
1
|
+
# nurture
|
|
2
|
+
|
|
3
|
+
Lifecycle email series for people who **agreed to hear from you** (welcome, onboarding, re-engagement). A nurture
|
|
4
|
+
is a sequence of kind `nurture`: a few emails sent from **one pinned mailbox** to an audience **list**, stopping per
|
|
5
|
+
contact when they reply. It is not cold outreach (use the `sequencer` block for that). Going live reaches real people,
|
|
6
|
+
so launch is tier `external` and is confirmed with the audience size.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
npx @graph8/sdk add nurture
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Installs `foundation`, `crm` and `lists` first if missing. Everything lands under `g8/nurture/`.
|
|
15
|
+
|
|
16
|
+
## Prerequisites (the human must do these)
|
|
17
|
+
|
|
18
|
+
| Where | What | How to verify |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| graph8 | Nurture is controlled by a platform-wide switch (`ENABLE_NURTURE`), which is **on** in graph8 production. If it is ever switched off, every `/nurtures` call answers `404 "Nurture is not enabled"`; the components show a "not enabled" panel instead of an error. Nothing to request per workspace. | `GET /api/v1/nurtures` returns 200. |
|
|
21
|
+
| graph8 app | A **connected mailbox**. Google and Microsoft sign-in only works inside the app, never through the API. | `GET /api/v1/mailboxes/my` lists a non-archived mailbox. |
|
|
22
|
+
| The business | A **consented audience**: a list of people who opted in. A person ticks the consent box before Go live. | The owner can say where and when each contact opted in. |
|
|
23
|
+
| graph8 key | `sequences:run` to launch (separate from `sequences:write`). | `g8.api.me.describeCurrentKey()` |
|
|
24
|
+
| Your app | `allowedPrefixes` must include `nurtures`, `mailboxes`, `lists`, `analytics`. | The route file lists them. |
|
|
25
|
+
|
|
26
|
+
## The flow, in order (IDs passed between steps)
|
|
27
|
+
|
|
28
|
+
1. **Sender**: `listMyMailboxes` -> pick one -> **`mailbox.id`**, `mailbox.email`, `mailbox.channel_type`.
|
|
29
|
+
2. **Audience**: a list id (`listLists`, or create one with the `lists` block) -> **`list.id`**.
|
|
30
|
+
3. **Create**: `createNurture({ name, pinned_mailbox_id: mailbox.id, associated_list_id: list.id, steps, channels })` -> **`nurture.id`** (a string).
|
|
31
|
+
- `steps[]`: `{ step_order, step_type: "EMAIL", input_type: "MANUAL_TEMPLATE", time_interval: <seconds before the step>, step_data: { subject, body, email_type: "plain" } }`. Step 1 has `time_interval: 0`.
|
|
32
|
+
- `channels[]`: exactly the pinned mailbox: `{ channel_id: mailbox.id, channel_value: mailbox.email, channel_type: mailbox.channel_type }`. Any other email channel is rejected.
|
|
33
|
+
- The result is a **draft**. The mailbox and the steps cannot be changed afterwards through this API; to change them, `duplicateNurture` and rebuild.
|
|
34
|
+
4. **Audience membership**: "enrolling" a contact = `addContactsToList(list.id, { contact_ids })`. `updateNurture({ associated_list_id })` re-points the nurture at another list (drafts only in the UI).
|
|
35
|
+
5. **Launch**: `NurtureEnroll` shows the audience size (`contact_count`), requires the consent box, then `launchNurture(nurture.id)` behind `ConfirmSpendDialog`.
|
|
36
|
+
It runs the same enrollment, schedule and consent checks as the app's Go Live button.
|
|
37
|
+
6. **Run**: `pauseNurture` (immediate), `resumeNurture` (confirmed here, because sending restarts), `duplicateNurture`, `deleteNurture` (archives; a live one is wound down first).
|
|
38
|
+
7. **Results**: `NurtureStats` reads `getNurture`, the audience size, and `getSequencePerformanceById` (contacts, emails sent, replies, meetings; cached about 5 minutes).
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
// app/nurtures/page.tsx
|
|
42
|
+
"use client";
|
|
43
|
+
import { useState } from "react";
|
|
44
|
+
import { NurtureBuilder } from "@/g8/nurture/components/nurture-builder";
|
|
45
|
+
import { NurtureEnroll } from "@/g8/nurture/components/nurture-enroll";
|
|
46
|
+
import { NurtureStats } from "@/g8/nurture/components/nurture-stats";
|
|
47
|
+
|
|
48
|
+
export default function Page() {
|
|
49
|
+
const [id, setId] = useState<string | null>(null);
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<NurtureBuilder onSelectNurture={(n) => setId(n.id)} />
|
|
53
|
+
{id ? (<><NurtureEnroll nurtureId={id} /><NurtureStats nurtureId={id} /></>) : null}
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Default decisions (do not ask the human unless marked)
|
|
60
|
+
|
|
61
|
+
| Decision | Default | Ask? |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| Reply behaviour | Stop a contact's series when they reply | no |
|
|
64
|
+
| Opt-out line | Always on (`include_unsubscribe_cta: true`); never turn it off | no |
|
|
65
|
+
| Wait between emails | 3 days (the API takes seconds: days x 86400) | no |
|
|
66
|
+
| Which mailbox sends | The first connected mailbox of the key's owner | **yes** |
|
|
67
|
+
| Launching | Never without the audience size confirmed and the consent box ticked | no |
|
|
68
|
+
|
|
69
|
+
## Things that surprise people
|
|
70
|
+
|
|
71
|
+
- **`pinned_mailbox_id` is required and immutable.** A nurture without one is rejected; an email channel on another mailbox is rejected.
|
|
72
|
+
- **There is no nurture analytics endpoint and no enroll endpoint.** Enrollment is list membership; results come from the outbound sequence report (an empty report is "no sends yet", not an error).
|
|
73
|
+
- **`createNurture`, `updateNurture`, `pauseNurture`, `resumeNurture`, `duplicateNurture` are tier `write`; only `launchNurture` is `external`.** The block still confirms resume, because it restarts real sending.
|
|
74
|
+
- **`is_shared: true` is a 403 for API keys today** (org-wide sharing needs an Admin role the API key path does not carry). Nurtures made through the API are personal.
|
|
75
|
+
- **Status names**: a new nurture is `drafted`; running ones read as live/paused; finished ones as completed/terminated. The helpers match loosely (`isDrafted`, `isLiveLike`).
|
|
76
|
+
|
|
77
|
+
## Recipes (server-side, `recipes/`)
|
|
78
|
+
|
|
79
|
+
- `create-a-nurture.ts`: mailbox, audience list, drafted nurture with two emails.
|
|
80
|
+
- `enroll-contacts-and-launch.ts`: add consented contacts; launch only with explicit approval.
|
|
81
|
+
- `nurture-health-check.ts`: status and recent results for every nurture.
|
|
82
|
+
|
|
83
|
+
## What is in the block
|
|
84
|
+
|
|
85
|
+
| Component | File | What it is |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `NurtureBuilder` | `components/nurture-builder.tsx` | List your nurtures; create, edit, pause, resume, duplicate and archive them. |
|
|
88
|
+
| `NurtureForm` | `components/nurture-form.tsx` | Create a nurture (name, sender mailbox, audience, email steps) or edit its settings. |
|
|
89
|
+
| `NurtureEnroll` | `components/nurture-enroll.tsx` | Choose the audience list, attest consent, optionally add selected contacts, and launch with the audience size confirmed. |
|
|
90
|
+
| `NurtureStats` | `components/nurture-stats.tsx` | Nurture status, audience size and reply/meeting results from the outbound sequence report. |
|
|
91
|
+
|
|
92
|
+
## Operations
|
|
93
|
+
|
|
94
|
+
Tiers: `read` and `write` need no confirmation; `external` (launch) and `destructive` (archive) always open `ConfirmSpendDialog` first.
|
|
95
|
+
|
|
96
|
+
| Operation | Method and path | Tier | Scope | Used by |
|
|
97
|
+
|---|---|---|---|---|
|
|
98
|
+
| `sequences.listNurtures` | `GET /api/v1/nurtures` | read | `sequences:read` | NurtureBuilder |
|
|
99
|
+
| `sequences.pauseNurture` | `POST /api/v1/nurtures/{nurture_id}/pause` | write | `sequences:write` | NurtureBuilder |
|
|
100
|
+
| `sequences.resumeNurture` | `POST /api/v1/nurtures/{nurture_id}/resume` | write | `sequences:write` | NurtureBuilder |
|
|
101
|
+
| `sequences.duplicateNurture` | `POST /api/v1/nurtures/{nurture_id}/duplicate` | write | `sequences:write` | NurtureBuilder |
|
|
102
|
+
| `sequences.deleteNurture` | `DELETE /api/v1/nurtures/{nurture_id}` | destructive | `sequences:delete` | NurtureBuilder |
|
|
103
|
+
| `mailboxes.listMyMailboxes` | `GET /api/v1/mailboxes/my` | read | `campaigns:read` | NurtureForm |
|
|
104
|
+
| `lists.listLists` | `GET /api/v1/lists` | read | `lists:read` | NurtureForm, NurtureEnroll, NurtureStats |
|
|
105
|
+
| `sequences.createNurture` | `POST /api/v1/nurtures` | write | `sequences:write` | NurtureForm |
|
|
106
|
+
| `sequences.updateNurture` | `PATCH /api/v1/nurtures/{nurture_id}` | write | `sequences:write` | NurtureForm, NurtureEnroll |
|
|
107
|
+
| `sequences.getNurture` | `GET /api/v1/nurtures/{nurture_id}` | read | `sequences:read` | NurtureEnroll, NurtureStats |
|
|
108
|
+
| `lists.addContactsToList` | `POST /api/v1/lists/{list_id}/contacts` | write | `lists:write` | NurtureEnroll |
|
|
109
|
+
| `sequences.launchNurture` | `POST /api/v1/nurtures/{nurture_id}/launch` | external | `sequences:run` | NurtureEnroll |
|
|
110
|
+
| `analytics.getSequencePerformanceById` | `GET /api/v1/analytics/outbound/sequence-performance/{sequence_id}` | read | `analytics:read` | NurtureStats |
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../block.schema.json",
|
|
3
|
+
"id": "nurture",
|
|
4
|
+
"title": "Nurture",
|
|
5
|
+
"summary": "Build email nurtures for consented audiences: create, choose the audience, launch with confirmation, pause, resume, copy and read results.",
|
|
6
|
+
"features": [
|
|
7
|
+
"create a nurture with emails and one pinned mailbox",
|
|
8
|
+
"audience list and consent attestation before launch",
|
|
9
|
+
"launch confirmed with the audience size",
|
|
10
|
+
"pause, resume, duplicate, archive",
|
|
11
|
+
"status, audience and reply results"
|
|
12
|
+
],
|
|
13
|
+
"dependsOn": [
|
|
14
|
+
"lists"
|
|
15
|
+
],
|
|
16
|
+
"operations": [
|
|
17
|
+
"list_nurtures_nurtures_get",
|
|
18
|
+
"pause_nurture_nurtures__nurture_id__pause_post",
|
|
19
|
+
"resume_nurture_nurtures__nurture_id__resume_post",
|
|
20
|
+
"duplicate_nurture_nurtures__nurture_id__duplicate_post",
|
|
21
|
+
"delete_nurture_nurtures__nurture_id__delete",
|
|
22
|
+
"list_my_mailboxes_mailboxes_my_get",
|
|
23
|
+
"list_lists_lists_get",
|
|
24
|
+
"create_nurture_nurtures_post",
|
|
25
|
+
"update_nurture_nurtures__nurture_id__patch",
|
|
26
|
+
"get_nurture_nurtures__nurture_id__get",
|
|
27
|
+
"add_contacts_to_list_lists__list_id__contacts_post",
|
|
28
|
+
"launch_nurture_nurtures__nurture_id__launch_post",
|
|
29
|
+
"get_sequence_performance_by_id_analytics_outbound_sequence_performance__sequence_id__get"
|
|
30
|
+
],
|
|
31
|
+
"components": [
|
|
32
|
+
{
|
|
33
|
+
"name": "NurtureBuilder",
|
|
34
|
+
"file": "components/nurture-builder.tsx",
|
|
35
|
+
"purpose": "List your nurtures; create, edit, pause, resume, duplicate and archive them.",
|
|
36
|
+
"operations": [
|
|
37
|
+
"list_nurtures_nurtures_get",
|
|
38
|
+
"pause_nurture_nurtures__nurture_id__pause_post",
|
|
39
|
+
"resume_nurture_nurtures__nurture_id__resume_post",
|
|
40
|
+
"duplicate_nurture_nurtures__nurture_id__duplicate_post",
|
|
41
|
+
"delete_nurture_nurtures__nurture_id__delete"
|
|
42
|
+
],
|
|
43
|
+
"tiers": [
|
|
44
|
+
"read",
|
|
45
|
+
"write",
|
|
46
|
+
"destructive"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "NurtureForm",
|
|
51
|
+
"file": "components/nurture-form.tsx",
|
|
52
|
+
"purpose": "Create a nurture (name, sender mailbox, audience, email steps) or edit its settings.",
|
|
53
|
+
"operations": [
|
|
54
|
+
"list_my_mailboxes_mailboxes_my_get",
|
|
55
|
+
"list_lists_lists_get",
|
|
56
|
+
"create_nurture_nurtures_post",
|
|
57
|
+
"update_nurture_nurtures__nurture_id__patch"
|
|
58
|
+
],
|
|
59
|
+
"tiers": [
|
|
60
|
+
"read",
|
|
61
|
+
"write"
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "NurtureEnroll",
|
|
66
|
+
"file": "components/nurture-enroll.tsx",
|
|
67
|
+
"purpose": "Choose the audience list, attest consent, optionally add selected contacts, and launch with the audience size confirmed.",
|
|
68
|
+
"operations": [
|
|
69
|
+
"get_nurture_nurtures__nurture_id__get",
|
|
70
|
+
"list_lists_lists_get",
|
|
71
|
+
"update_nurture_nurtures__nurture_id__patch",
|
|
72
|
+
"add_contacts_to_list_lists__list_id__contacts_post",
|
|
73
|
+
"launch_nurture_nurtures__nurture_id__launch_post"
|
|
74
|
+
],
|
|
75
|
+
"tiers": [
|
|
76
|
+
"read",
|
|
77
|
+
"write",
|
|
78
|
+
"external"
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "NurtureStats",
|
|
83
|
+
"file": "components/nurture-stats.tsx",
|
|
84
|
+
"purpose": "Nurture status, audience size and reply/meeting results from the outbound sequence report.",
|
|
85
|
+
"operations": [
|
|
86
|
+
"get_nurture_nurtures__nurture_id__get",
|
|
87
|
+
"list_lists_lists_get",
|
|
88
|
+
"get_sequence_performance_by_id_analytics_outbound_sequence_performance__sequence_id__get"
|
|
89
|
+
],
|
|
90
|
+
"tiers": [
|
|
91
|
+
"read"
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"hooks": [],
|
|
96
|
+
"lib": [
|
|
97
|
+
{
|
|
98
|
+
"file": "lib/nurture-types.ts",
|
|
99
|
+
"purpose": "Nurture/mailbox/list types, status predicates, and the step and channel payload builders."
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
"recipes": [
|
|
103
|
+
{
|
|
104
|
+
"file": "recipes/create-a-nurture.ts",
|
|
105
|
+
"title": "Create a drafted nurture with its emails, pinned to one mailbox",
|
|
106
|
+
"when": "Setting up a lifecycle series for consented contacts."
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"file": "recipes/enroll-contacts-and-launch.ts",
|
|
110
|
+
"title": "Add consented contacts to the audience and, with approval, launch",
|
|
111
|
+
"when": "A contact opted in and should start the series; launching needs a person's yes."
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"file": "recipes/nurture-health-check.ts",
|
|
115
|
+
"title": "Check every nurture's status and recent results",
|
|
116
|
+
"when": "A daily check or dashboard backend."
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
"prerequisites": [
|
|
120
|
+
{
|
|
121
|
+
"id": "nurture-enabled",
|
|
122
|
+
"description": "Nurture is behind a feature flag: every nurtures call answers 404 \"Nurture is not enabled\" until graph8 turns it on for the workspace. Components show an explanation instead of an error.",
|
|
123
|
+
"where": "api",
|
|
124
|
+
"verify": "GET /api/v1/nurtures returns 200."
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"id": "connected-mailbox",
|
|
128
|
+
"description": "At least one mailbox connected in the graph8 app. A nurture is pinned to ONE mailbox. Google and Microsoft sign-in can only be done in the app.",
|
|
129
|
+
"where": "api",
|
|
130
|
+
"verify": "GET /api/v1/mailboxes/my lists a non-archived mailbox."
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"id": "consented-audience",
|
|
134
|
+
"description": "A list of people who agreed to hear from you. A nurture is for consented audiences, not cold outreach (use the sequencer for that). A human confirms consent before launch.",
|
|
135
|
+
"where": "app",
|
|
136
|
+
"verify": "The owner confirmed where and when each contact opted in."
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"id": "launch-scope",
|
|
140
|
+
"description": "Launching needs the sequences:run scope (an external operation), separate from sequences:write.",
|
|
141
|
+
"where": "api",
|
|
142
|
+
"verify": "g8.api.me.describeCurrentKey() lists sequences:run."
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"id": "proxy-allowlist",
|
|
146
|
+
"description": "Add \"nurtures\", \"mailboxes\", \"lists\" and \"analytics\" to allowedPrefixes in app/api/g8/[...path]/route.ts.",
|
|
147
|
+
"where": "app",
|
|
148
|
+
"verify": "The route file's allowedPrefixes contains them."
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
"scopes": [
|
|
152
|
+
"analytics:read",
|
|
153
|
+
"campaigns:read",
|
|
154
|
+
"lists:read",
|
|
155
|
+
"lists:write",
|
|
156
|
+
"sequences:delete",
|
|
157
|
+
"sequences:read",
|
|
158
|
+
"sequences:run",
|
|
159
|
+
"sequences:write"
|
|
160
|
+
],
|
|
161
|
+
"defaults": [
|
|
162
|
+
{
|
|
163
|
+
"decision": "Reply behaviour",
|
|
164
|
+
"default": "Stop nurturing a contact when they reply (finish_on_reply true)",
|
|
165
|
+
"askHuman": false
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"decision": "Opt-out line",
|
|
169
|
+
"default": "Append the opt-out line to every email (include_unsubscribe_cta true); never turn it off",
|
|
170
|
+
"askHuman": false
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"decision": "Wait between emails",
|
|
174
|
+
"default": "3 days (the API takes seconds: days x 86400)",
|
|
175
|
+
"askHuman": false
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"decision": "Which mailbox sends",
|
|
179
|
+
"default": "The first connected mailbox of the key's owner",
|
|
180
|
+
"askHuman": true
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"decision": "Launching",
|
|
184
|
+
"default": "Never launch without the person confirming the audience size and the consent box",
|
|
185
|
+
"askHuman": false
|
|
186
|
+
}
|
|
187
|
+
],
|
|
188
|
+
"peerDependencies": {
|
|
189
|
+
"react": ">=18"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NurtureBuilder: your nurtures, create a new one, edit, pause, resume, copy and archive.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. listNurtures (GET /nurtures?page&limit) read (your own plus org-shared)
|
|
8
|
+
* 2. NurtureForm: listMyMailboxes, listLists, createNurture, updateNurture (see that file)
|
|
9
|
+
* 3. pauseNurture (POST /nurtures/{id}/pause) write (stops further sends)
|
|
10
|
+
* 4. resumeNurture (POST /nurtures/{id}/resume) write -> confirmed anyway: sending restarts to real people
|
|
11
|
+
* 5. duplicateNurture (POST /nurtures/{id}/duplicate) write (a drafted copy owned by you, no channels)
|
|
12
|
+
* 6. deleteNurture (DELETE /nurtures/{id}) destructive -> ConfirmSpendDialog (archives; a live one is wound down first)
|
|
13
|
+
* Going live is NOT here: NurtureEnroll launches (external) after the audience and consent are confirmed.
|
|
14
|
+
* The API is behind a feature flag and answers 404 "Nurture is not enabled" until it is on.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "@graph8/sdk/ui";
|
|
19
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
21
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
22
|
+
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
23
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
24
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
|
+
import { isFinished, isLiveLike, isNurtureOff, isPaused, statusOf } from "../lib/nurture-types";
|
|
26
|
+
import type { Nurture } from "../lib/nurture-types";
|
|
27
|
+
import { NurtureForm } from "./nurture-form";
|
|
28
|
+
|
|
29
|
+
export interface NurtureBuilderProps {
|
|
30
|
+
/** Called when a nurture is chosen for launch/enrollment (render NurtureEnroll with its id). */
|
|
31
|
+
onSelectNurture?: (nurture: Nurture) => void;
|
|
32
|
+
pageSize?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type Action = { kind: "pause" | "duplicate"; nurture: Nurture };
|
|
36
|
+
|
|
37
|
+
export function NurtureBuilder({ onSelectNurture, pageSize = 50 }: NurtureBuilderProps) {
|
|
38
|
+
const [form, setForm] = useState<{ nurture?: Nurture } | null>(null);
|
|
39
|
+
const [toArchive, setToArchive] = useState<Nurture | null>(null);
|
|
40
|
+
const [toResume, setToResume] = useState<Nurture | null>(null);
|
|
41
|
+
const list = useG8Query((signal) => g8Get<{ nurtures?: Nurture[] }>("/nurtures", { page: 1, limit: pageSize }, { signal }).then((r) => r.data?.nurtures ?? []), [pageSize]);
|
|
42
|
+
const [busy, setBusy] = useState<string | null>(null);
|
|
43
|
+
const act = useG8Mutation((a: Action) => g8Post(`/nurtures/${encodeURIComponent(a.nurture.id)}/${a.kind}`, a.kind === "duplicate" ? {} : undefined), { onSuccess: () => void list.refetch() });
|
|
44
|
+
const run = async (a: Action): Promise<void> => {
|
|
45
|
+
setBusy(a.nurture.id);
|
|
46
|
+
await act.mutate(a);
|
|
47
|
+
setBusy(null);
|
|
48
|
+
};
|
|
49
|
+
const resume = useG8Mutation((n: Nurture) => g8Post(`/nurtures/${encodeURIComponent(n.id)}/resume`), { onSuccess: () => { setToResume(null); void list.refetch(); } });
|
|
50
|
+
const archive = useG8Mutation((n: Nurture) => g8Delete(`/nurtures/${encodeURIComponent(n.id)}`), { onSuccess: () => { setToArchive(null); void list.refetch(); } });
|
|
51
|
+
|
|
52
|
+
if (isNurtureOff(list.error)) return <EmptyState title="Nurture is not enabled" description="Nurture is behind a feature flag for this workspace. Ask graph8 to turn it on, then reload." />;
|
|
53
|
+
if (list.error) return <ApiError error={list.error} title="Could not load nurtures" onRetry={() => void list.refetch()} />;
|
|
54
|
+
if (form) {
|
|
55
|
+
return (
|
|
56
|
+
<Card>
|
|
57
|
+
<CardHeader><CardTitle>{form.nurture ? `Edit ${form.nurture.name}` : "New nurture"}</CardTitle></CardHeader>
|
|
58
|
+
<CardContent><NurtureForm nurture={form.nurture} onCancel={() => setForm(null)} onSaved={() => { setForm(null); void list.refetch(); }} /></CardContent>
|
|
59
|
+
</Card>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const nurtures = list.data ?? [];
|
|
63
|
+
return (
|
|
64
|
+
<div className="grid gap-3">
|
|
65
|
+
<div className="flex items-center justify-between">
|
|
66
|
+
<h2 className="text-lg font-semibold">Nurtures</h2>
|
|
67
|
+
<Button onClick={() => setForm({})}>New nurture</Button>
|
|
68
|
+
</div>
|
|
69
|
+
{act.error ? <ApiError error={act.error} title="That did not work" /> : null}
|
|
70
|
+
{list.loading && !list.data ? <div aria-busy="true" className="grid gap-2"><Skeleton className="h-14 w-full" /><Skeleton className="h-14 w-full" /></div> : nurtures.length === 0 ? (
|
|
71
|
+
<EmptyState title="No nurtures yet" description="A nurture sends a short series of emails to people who agreed to hear from you." action={<Button onClick={() => setForm({})}>New nurture</Button>} />
|
|
72
|
+
) : (
|
|
73
|
+
<ul className="grid gap-2">
|
|
74
|
+
{nurtures.map((n) => (
|
|
75
|
+
<li key={n.id} className="flex flex-wrap items-center justify-between gap-2 rounded-md border p-3">
|
|
76
|
+
<div>
|
|
77
|
+
<p className="font-medium">{n.name}</p>
|
|
78
|
+
<p className="text-muted-foreground text-xs">{statusOf(n)}{n.is_shared ? " · shared" : ""}</p>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="flex flex-wrap gap-1">
|
|
81
|
+
{onSelectNurture ? <Button size="sm" variant="outline" onClick={() => onSelectNurture(n)}>Audience and launch</Button> : null}
|
|
82
|
+
{isLiveLike(n) ? <Button size="sm" variant="ghost" loading={busy === n.id} disabled={busy !== null} onClick={() => void run({ kind: "pause", nurture: n })}>Pause</Button> : null}
|
|
83
|
+
{isPaused(n) ? <Button size="sm" variant="ghost" onClick={() => setToResume(n)}>Resume</Button> : null}
|
|
84
|
+
{!isFinished(n) ? <Button size="sm" variant="ghost" onClick={() => setForm({ nurture: n })}>Edit</Button> : null}
|
|
85
|
+
<Button size="sm" variant="ghost" disabled={busy !== null} onClick={() => void run({ kind: "duplicate", nurture: n })}>Duplicate</Button>
|
|
86
|
+
{!isFinished(n) ? <Button size="sm" variant="ghost" onClick={() => setToArchive(n)}>Archive</Button> : null}
|
|
87
|
+
</div>
|
|
88
|
+
</li>
|
|
89
|
+
))}
|
|
90
|
+
</ul>
|
|
91
|
+
)}
|
|
92
|
+
<ConfirmSpendDialog
|
|
93
|
+
open={toResume !== null}
|
|
94
|
+
tier="external"
|
|
95
|
+
title={`Resume ${toResume?.name ?? "this nurture"}?`}
|
|
96
|
+
description={<><p>Emails start going out again to the contacts who have not finished it.</p>{resume.error ? <ApiError error={resume.error} title="Could not resume" /> : null}</>}
|
|
97
|
+
confirmLabel="Resume"
|
|
98
|
+
confirming={resume.loading}
|
|
99
|
+
onCancel={() => setToResume(null)}
|
|
100
|
+
onConfirm={() => { if (toResume) void resume.mutate(toResume); }}
|
|
101
|
+
/>
|
|
102
|
+
<ConfirmSpendDialog
|
|
103
|
+
open={toArchive !== null}
|
|
104
|
+
tier="destructive"
|
|
105
|
+
title={`Archive ${toArchive?.name ?? "this nurture"}?`}
|
|
106
|
+
description={<><p>{toArchive && isLiveLike(toArchive) ? "It is paused and wound down first, then archived. Contacts stop receiving its emails." : "It is archived and no longer runs."}</p>{archive.error ? <ApiError error={archive.error} title="Could not archive" /> : null}</>}
|
|
107
|
+
confirmLabel="Archive"
|
|
108
|
+
confirming={archive.loading}
|
|
109
|
+
onCancel={() => setToArchive(null)}
|
|
110
|
+
onConfirm={() => { if (toArchive) void archive.mutate(toArchive); }}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NurtureEnroll: choose who a nurture engages, confirm they consented, and take it live.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getNurture (GET /nurtures/{id}) read
|
|
8
|
+
* 2. listLists (GET /lists) read (the audience; its contact_count is the recipient count)
|
|
9
|
+
* 3. updateNurture (PATCH /nurtures/{id}, associated_list_id) write (point the nurture at a list)
|
|
10
|
+
* 4. addContactsToList (POST /lists/{list_id}/contacts) write (optional: add specific contacts to the audience)
|
|
11
|
+
* 5. launchNurture (POST /nurtures/{id}/launch) external -> ConfirmSpendDialog with the audience size
|
|
12
|
+
* "Enrolling" a contact means putting them in the nurture's audience list; the nurture engages that list once launched.
|
|
13
|
+
* Launch runs the same enrollment, schedule and consent checks as the app's Go Live button. The consent box is a
|
|
14
|
+
* human attestation in YOUR UI (the API does not take one): keep it, it is what keeps this a nurture and not spam.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useEffect, useState } from "react";
|
|
18
|
+
import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "@graph8/sdk/ui";
|
|
19
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
21
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
22
|
+
import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
23
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
24
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
|
+
import { isDrafted, isNurtureMissing, isNurtureOff, statusOf } from "../lib/nurture-types";
|
|
26
|
+
import type { ListOption, Nurture } from "../lib/nurture-types";
|
|
27
|
+
|
|
28
|
+
export interface NurtureEnrollProps {
|
|
29
|
+
nurtureId: string;
|
|
30
|
+
/** Contact ids (for example selected in a contacts table) to add to the audience list. */
|
|
31
|
+
contactIds?: readonly number[];
|
|
32
|
+
onLaunched?: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function NurtureEnroll({ nurtureId, contactIds = [], onLaunched }: NurtureEnrollProps) {
|
|
36
|
+
const base = `/nurtures/${encodeURIComponent(nurtureId)}`;
|
|
37
|
+
const nurture = useG8Query((signal) => g8Get<Nurture>(base, {}, { signal }).then((r) => r.data), [base]);
|
|
38
|
+
const lists = useG8Query((signal) => g8Get<ListOption[]>("/lists", { limit: 200 }, { signal }).then((r) => r.data ?? []), []);
|
|
39
|
+
const [listId, setListId] = useState("");
|
|
40
|
+
const [consent, setConsent] = useState(false);
|
|
41
|
+
const [confirming, setConfirming] = useState(false);
|
|
42
|
+
|
|
43
|
+
// Consent and a half-chosen list belong to ONE nurture.
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
setConsent(false);
|
|
46
|
+
setConfirming(false);
|
|
47
|
+
setListId("");
|
|
48
|
+
}, [nurtureId]);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (nurture.data?.associated_list_id) setListId(String(nurture.data.associated_list_id));
|
|
52
|
+
}, [nurture.data?.associated_list_id]);
|
|
53
|
+
|
|
54
|
+
const setAudience = useG8Mutation((id: number) => g8Patch<Nurture>(base, { associated_list_id: id }), { onSuccess: () => void nurture.refetch() });
|
|
55
|
+
const addContacts = useG8Mutation((id: number) => g8Post(`/lists/${id}/contacts`, { contact_ids: [...contactIds] }), { onSuccess: () => void lists.refetch() });
|
|
56
|
+
const launch = useG8Mutation(() => g8Post(`${base}/launch`), { onSuccess: () => { setConfirming(false); void nurture.refetch(); onLaunched?.(); } });
|
|
57
|
+
|
|
58
|
+
if (isNurtureOff(nurture.error)) return <EmptyState title="Nurture is not enabled" description="Nurture is behind a feature flag for this workspace. Ask graph8 to turn it on." />;
|
|
59
|
+
if (isNurtureMissing(nurture.error)) return <EmptyState title="This nurture no longer exists" description="It may have been deleted, or it is not yours to see. Pick another nurture." />;
|
|
60
|
+
if (nurture.error) return <ApiError error={nurture.error} title="Could not load the nurture" onRetry={() => void nurture.refetch()} />;
|
|
61
|
+
if (!nurture.data) return <Skeleton className="h-40 w-full" aria-busy="true" />;
|
|
62
|
+
|
|
63
|
+
const list = (lists.data ?? []).find((l) => String(l.id) === listId);
|
|
64
|
+
const attached = nurture.data.associated_list_id ? String(nurture.data.associated_list_id) === listId : false;
|
|
65
|
+
const audience = list?.contact_count ?? 0;
|
|
66
|
+
const canLaunch = isDrafted(nurture.data) && attached && audience > 0 && consent;
|
|
67
|
+
return (
|
|
68
|
+
<Card>
|
|
69
|
+
<CardHeader><CardTitle>{nurture.data.name} <span className="text-muted-foreground text-sm font-normal">{statusOf(nurture.data)}</span></CardTitle></CardHeader>
|
|
70
|
+
<CardContent className="grid gap-4">
|
|
71
|
+
<div className="grid max-w-md gap-1">
|
|
72
|
+
<label htmlFor="g8-nurture-audience" className="text-sm font-medium">Audience list</label>
|
|
73
|
+
<select id="g8-nurture-audience" className="border-input bg-background h-9 rounded-md border px-2 text-sm" value={listId} disabled={!isDrafted(nurture.data)} onChange={(e) => setListId(e.target.value)}>
|
|
74
|
+
<option value="">Choose a list</option>
|
|
75
|
+
{(lists.data ?? []).map((l) => <option key={l.id} value={l.id}>{l.title} ({(l.contact_count ?? 0).toLocaleString()})</option>)}
|
|
76
|
+
</select>
|
|
77
|
+
<div className="flex gap-2">
|
|
78
|
+
<Button size="sm" variant="outline" loading={setAudience.loading} disabled={!listId || attached || !isDrafted(nurture.data)} onClick={() => void setAudience.mutate(Number(listId))}>Use this list</Button>
|
|
79
|
+
{contactIds.length > 0 ? <Button size="sm" variant="outline" loading={addContacts.loading} disabled={!attached} onClick={() => void addContacts.mutate(Number(listId))}>Add {contactIds.length} selected {contactIds.length === 1 ? "contact" : "contacts"}</Button> : null}
|
|
80
|
+
</div>
|
|
81
|
+
{attached ? <p className="text-sm">This nurture will engage <strong>{audience.toLocaleString()}</strong> {audience === 1 ? "contact" : "contacts"}.</p> : null}
|
|
82
|
+
</div>
|
|
83
|
+
{[setAudience.error, addContacts.error].filter(Boolean).map((e, i) => <ApiError key={i} error={e} title="That did not work" />)}
|
|
84
|
+
{isDrafted(nurture.data) ? (
|
|
85
|
+
<div className="grid gap-3">
|
|
86
|
+
<label className="flex items-start gap-2 text-sm">
|
|
87
|
+
<input type="checkbox" className="mt-1" checked={consent} onChange={(e) => setConsent(e.target.checked)} />
|
|
88
|
+
<span>Everyone in this list agreed to hear from us. I can show where and when.</span>
|
|
89
|
+
</label>
|
|
90
|
+
<div><Button disabled={!canLaunch} onClick={() => setConfirming(true)}>Go live</Button></div>
|
|
91
|
+
</div>
|
|
92
|
+
) : <p className="text-sm">This nurture is {statusOf(nurture.data)}. Use the Nurtures list to pause or resume it.</p>}
|
|
93
|
+
<ConfirmSpendDialog
|
|
94
|
+
open={confirming}
|
|
95
|
+
tier="external"
|
|
96
|
+
title={`Launch "${nurture.data.name}" to ${audience.toLocaleString()} contacts?`}
|
|
97
|
+
recipients={audience}
|
|
98
|
+
description={<><p>The first email goes out to real people as soon as the schedule allows, and follow-ups continue until each contact replies or finishes.</p>{launch.error ? <ApiError error={launch.error} title="Could not launch" /> : null}</>}
|
|
99
|
+
confirmLabel="Launch"
|
|
100
|
+
confirming={launch.loading}
|
|
101
|
+
onCancel={() => setConfirming(false)}
|
|
102
|
+
onConfirm={() => void launch.mutate(undefined)}
|
|
103
|
+
/>
|
|
104
|
+
</CardContent>
|
|
105
|
+
</Card>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NurtureForm: create a nurture (name, sender mailbox, audience, email steps) or edit its settings.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. listMyMailboxes (GET /mailboxes/my) read (a nurture is pinned to ONE mailbox, chosen at creation, immutable)
|
|
8
|
+
* 2. listLists (GET /lists) read (the consented audience)
|
|
9
|
+
* 3. createNurture (POST /nurtures) write (a DRAFT: nothing is sent until launch)
|
|
10
|
+
* 4. updateNurture (PATCH /nurtures/{id}) write (name, description, audience, reply/threading/unsubscribe flags)
|
|
11
|
+
* The steps and the sender cannot be changed after creation through this API: to change them, duplicate and recreate.
|
|
12
|
+
* Launching is NurtureEnroll's job, behind a confirmation with the audience size.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import { Button, Input, Label, Textarea } from "@graph8/sdk/ui";
|
|
17
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
18
|
+
import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
19
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
20
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
|
+
import { channelPayload, stepsPayload } from "../lib/nurture-types";
|
|
22
|
+
import type { ListOption, MailboxOption, Nurture, StepDraft } from "../lib/nurture-types";
|
|
23
|
+
|
|
24
|
+
export interface NurtureFormProps {
|
|
25
|
+
/** Edit this nurture; omit to create a new one. */
|
|
26
|
+
nurture?: Nurture;
|
|
27
|
+
onSaved: (nurture: Nurture) => void;
|
|
28
|
+
onCancel?: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm disabled:opacity-50";
|
|
32
|
+
|
|
33
|
+
export function NurtureForm({ nurture, onSaved, onCancel }: NurtureFormProps) {
|
|
34
|
+
const editing = Boolean(nurture);
|
|
35
|
+
const [name, setName] = useState(nurture?.name ?? "");
|
|
36
|
+
const [description, setDescription] = useState(nurture?.description ?? "");
|
|
37
|
+
const [mailboxId, setMailboxId] = useState<string>(nurture?.pinned_mailbox_id ? String(nurture.pinned_mailbox_id) : "");
|
|
38
|
+
const [listId, setListId] = useState<string>(nurture?.associated_list_id ? String(nurture.associated_list_id) : "");
|
|
39
|
+
const [finishOnReply, setFinishOnReply] = useState(nurture?.finish_on_reply ?? true);
|
|
40
|
+
const [sameThread, setSameThread] = useState(nurture?.send_in_same_thread ?? true);
|
|
41
|
+
const [unsubscribe, setUnsubscribe] = useState(nurture?.include_unsubscribe_cta ?? true);
|
|
42
|
+
const [steps, setSteps] = useState<StepDraft[]>([{ waitDays: 0, subject: "", body: "" }]);
|
|
43
|
+
const mailboxes = useG8Query((signal) => g8Get<{ mailboxes?: MailboxOption[] }>("/mailboxes/my", {}, { signal }).then((r) => (r.data?.mailboxes ?? []).filter((m) => !m.is_archived)), []);
|
|
44
|
+
const lists = useG8Query((signal) => g8Get<ListOption[]>("/lists", { limit: 200 }, { signal }).then((r) => r.data ?? []), []);
|
|
45
|
+
|
|
46
|
+
const mailbox = (mailboxes.data ?? []).find((m) => String(m.id) === mailboxId);
|
|
47
|
+
const stepsOk = steps.length > 0 && steps.every((s) => s.subject.trim() && s.body.trim());
|
|
48
|
+
const valid = name.trim() !== "" && (editing || (Boolean(mailbox) && stepsOk));
|
|
49
|
+
|
|
50
|
+
const save = useG8Mutation(async () => {
|
|
51
|
+
const flags = { finish_on_reply: finishOnReply, send_in_same_thread: sameThread, include_unsubscribe_cta: unsubscribe };
|
|
52
|
+
if (nurture) {
|
|
53
|
+
return (await g8Patch<Nurture>(`/nurtures/${encodeURIComponent(nurture.id)}`, { name: name.trim(), description: description.trim() || null, associated_list_id: listId ? Number(listId) : null, ...flags })).data;
|
|
54
|
+
}
|
|
55
|
+
return (await g8Post<Nurture>("/nurtures", {
|
|
56
|
+
name: name.trim(),
|
|
57
|
+
description: description.trim() || undefined,
|
|
58
|
+
pinned_mailbox_id: mailbox!.id,
|
|
59
|
+
associated_list_id: listId ? Number(listId) : undefined,
|
|
60
|
+
...flags,
|
|
61
|
+
steps: stepsPayload(steps),
|
|
62
|
+
channels: channelPayload(mailbox!),
|
|
63
|
+
})).data;
|
|
64
|
+
}, { onSuccess: onSaved });
|
|
65
|
+
|
|
66
|
+
const setStep = (i: number, patch: Partial<StepDraft>): void => setSteps((prev) => prev.map((s, at) => (at === i ? { ...s, ...patch } : s)));
|
|
67
|
+
return (
|
|
68
|
+
<form className="grid gap-3" onSubmit={(e) => { e.preventDefault(); if (valid) void save.mutate(undefined); }}>
|
|
69
|
+
<div><Label htmlFor="g8-nurture-name" className="mb-1">Name</Label><Input id="g8-nurture-name" value={name} onChange={(e) => setName(e.target.value)} /></div>
|
|
70
|
+
<div><Label htmlFor="g8-nurture-desc" className="mb-1">Description</Label><Textarea id="g8-nurture-desc" rows={2} value={description} onChange={(e) => setDescription(e.target.value)} /></div>
|
|
71
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
72
|
+
<div>
|
|
73
|
+
<Label htmlFor="g8-nurture-mailbox" className="mb-1">Send from (one mailbox)</Label>
|
|
74
|
+
<select id="g8-nurture-mailbox" className={SELECT} value={mailboxId} disabled={editing} onChange={(e) => setMailboxId(e.target.value)}>
|
|
75
|
+
<option value="">{mailboxes.loading ? "Loading..." : "Choose a mailbox"}</option>
|
|
76
|
+
{(mailboxes.data ?? []).map((m) => <option key={m.id} value={m.id}>{m.email}</option>)}
|
|
77
|
+
</select>
|
|
78
|
+
{!editing && !mailboxes.loading && (mailboxes.data ?? []).length === 0 ? <p className="mt-1 text-xs text-amber-700">No mailbox is connected. Connect one in graph8 first (Google and Microsoft sign-in happens in the app).</p> : null}
|
|
79
|
+
</div>
|
|
80
|
+
<div>
|
|
81
|
+
<Label htmlFor="g8-nurture-list" className="mb-1">Audience list (consented contacts)</Label>
|
|
82
|
+
<select id="g8-nurture-list" className={SELECT} value={listId} onChange={(e) => setListId(e.target.value)}>
|
|
83
|
+
<option value="">Choose later</option>
|
|
84
|
+
{(lists.data ?? []).map((l) => <option key={l.id} value={l.id}>{l.title}</option>)}
|
|
85
|
+
</select>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<fieldset className="flex flex-wrap gap-4 text-sm">
|
|
89
|
+
<legend className="sr-only">Behaviour</legend>
|
|
90
|
+
<label className="flex items-center gap-2"><input type="checkbox" checked={finishOnReply} onChange={(e) => setFinishOnReply(e.target.checked)} />Stop for a contact when they reply</label>
|
|
91
|
+
<label className="flex items-center gap-2"><input type="checkbox" checked={sameThread} onChange={(e) => setSameThread(e.target.checked)} />Keep follow-ups in one thread</label>
|
|
92
|
+
<label className="flex items-center gap-2"><input type="checkbox" checked={unsubscribe} onChange={(e) => setUnsubscribe(e.target.checked)} />Add an opt-out line to every email</label>
|
|
93
|
+
</fieldset>
|
|
94
|
+
{editing ? <p className="text-muted-foreground text-sm">The emails and the sender are fixed once created. To change them, duplicate this nurture and build the copy.</p> : (
|
|
95
|
+
<div className="grid gap-3">
|
|
96
|
+
<h3 className="text-sm font-semibold">Emails</h3>
|
|
97
|
+
{steps.map((s, i) => (
|
|
98
|
+
<div key={i} className="grid gap-2 rounded-md border p-3">
|
|
99
|
+
<div className="flex items-end gap-2">
|
|
100
|
+
<span className="text-sm font-medium">Email {i + 1}</span>
|
|
101
|
+
{i > 0 ? (
|
|
102
|
+
<div className="w-40"><Label htmlFor={`g8-step-wait-${i}`} className="mb-1">Wait (days)</Label><Input id={`g8-step-wait-${i}`} type="number" min={0} value={s.waitDays} onChange={(e) => setStep(i, { waitDays: Number(e.target.value) })} /></div>
|
|
103
|
+
) : <span className="text-muted-foreground text-xs">Sent first, when you launch</span>}
|
|
104
|
+
{steps.length > 1 ? <Button type="button" size="sm" variant="ghost" onClick={() => setSteps((prev) => prev.filter((_, at) => at !== i))}>Remove</Button> : null}
|
|
105
|
+
</div>
|
|
106
|
+
<Input aria-label={`Subject ${i + 1}`} placeholder="Subject" value={s.subject} onChange={(e) => setStep(i, { subject: e.target.value })} />
|
|
107
|
+
<Textarea aria-label={`Body ${i + 1}`} rows={4} placeholder="Message" value={s.body} onChange={(e) => setStep(i, { body: e.target.value })} />
|
|
108
|
+
</div>
|
|
109
|
+
))}
|
|
110
|
+
<div><Button type="button" size="sm" variant="outline" onClick={() => setSteps((prev) => [...prev, { waitDays: 3, subject: "", body: "" }])}>Add an email</Button></div>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
{save.error ? <ApiError error={save.error} title="Could not save the nurture" /> : null}
|
|
114
|
+
<div className="flex gap-2">
|
|
115
|
+
<Button type="submit" loading={save.loading} disabled={!valid}>{editing ? "Save changes" : "Create nurture"}</Button>
|
|
116
|
+
{onCancel ? <Button type="button" variant="outline" onClick={onCancel}>Cancel</Button> : null}
|
|
117
|
+
</div>
|
|
118
|
+
</form>
|
|
119
|
+
);
|
|
120
|
+
}
|