@graph8/sdk 0.361.0 → 0.370.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 +14 -10
- package/blocks/_foundation/README.md +172 -1
- package/blocks/_foundation/block.json +234 -13
- package/blocks/_foundation/components/api-error.tsx +71 -25
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +39 -7
- package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
- package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
- package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
- package/blocks/_foundation/components/screen/index.ts +22 -0
- package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
- package/blocks/_foundation/components/screen/section-card.tsx +47 -0
- package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
- package/blocks/_foundation/components/screen/split-view.tsx +125 -0
- package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
- package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-state.ts +74 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +14 -0
- package/blocks/agents/block.json +33 -1
- package/blocks/agents/components/agent-builder.tsx +13 -8
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
- package/blocks/agents/components/agent-run-detail.tsx +102 -0
- package/blocks/agents/components/agent-runs.tsx +16 -7
- package/blocks/agents/components/agents-screen.tsx +184 -0
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +209 -0
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +23 -9
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +21 -1
- package/blocks/appointments/block.json +43 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +77 -0
- package/blocks/appointments/components/meeting-list.tsx +14 -6
- package/blocks/appointments/components/meetings-screen.tsx +157 -0
- package/blocks/appointments/components/scheduling-screen.tsx +122 -0
- package/blocks/appointments/hooks/use-event-types.ts +17 -0
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +26 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +16 -5
- package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +154 -0
- package/blocks/campaigns/components/icp-editor.tsx +15 -15
- package/blocks/campaigns/components/persona-detail.tsx +172 -0
- package/blocks/campaigns/components/persona-editor.tsx +14 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
- package/blocks/crm/README.md +28 -0
- package/blocks/crm/block.json +58 -2
- package/blocks/crm/components/companies-screen.tsx +345 -0
- package/blocks/crm/components/company-detail.tsx +8 -6
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +6 -3
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +359 -0
- package/blocks/crm/components/dedupe-review.tsx +7 -5
- package/blocks/crm/components/import-wizard.tsx +4 -2
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-form.tsx +5 -3
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +379 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +7 -1
- package/blocks/crm/lib/crm-types.ts +12 -2
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +34 -3
- package/blocks/deals/block.json +45 -4
- package/blocks/deals/components/deal-detail.tsx +27 -13
- package/blocks/deals/components/deal-related.tsx +87 -0
- package/blocks/deals/components/deals-screen.tsx +298 -0
- package/blocks/deals/components/pipeline-board.tsx +4 -2
- package/blocks/deals/components/stage-editor.tsx +4 -4
- package/blocks/deals/hooks/use-default-owner.ts +60 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deals-types.ts +14 -0
- package/blocks/dialer/README.md +34 -2
- package/blocks/dialer/block.json +67 -1
- package/blocks/dialer/components/call-contact-button.tsx +252 -0
- package/blocks/dialer/components/call-log-screen.tsx +205 -0
- package/blocks/dialer/components/call-log.tsx +7 -3
- package/blocks/dialer/components/call-queue.tsx +1 -1
- package/blocks/dialer/components/dialer-screen.tsx +319 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/session-start-form.tsx +4 -2
- package/blocks/dialer/components/softphone.tsx +3 -1
- package/blocks/enrichment/README.md +14 -2
- package/blocks/enrichment/block.json +32 -4
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
- package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
- package/blocks/enrichment/components/waterfall-config.tsx +39 -10
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +12 -0
- package/blocks/forms/block.json +39 -17
- package/blocks/forms/components/form-builder.tsx +3 -1
- package/blocks/forms/components/form-embed.tsx +3 -1
- package/blocks/forms/components/forms-screen.tsx +353 -0
- package/blocks/forms/components/submissions-table.tsx +16 -6
- package/blocks/inbox/README.md +27 -3
- package/blocks/inbox/block.json +14 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +9 -0
- package/blocks/integrations/block.json +46 -1
- package/blocks/integrations/components/crm-detail.tsx +100 -0
- package/blocks/integrations/components/integration-list.tsx +9 -4
- package/blocks/integrations/components/integrations-screen.tsx +135 -0
- package/blocks/integrations/components/sync-status.tsx +5 -3
- package/blocks/integrations/components/webhook-detail.tsx +160 -0
- package/blocks/integrations/components/webhook-manager.tsx +13 -8
- package/blocks/integrations/lib/integrations-utils.ts +6 -1
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/intent-feed.tsx +5 -2
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/lifecycle/README.md +16 -0
- package/blocks/lifecycle/block.json +34 -7
- package/blocks/lifecycle/components/account-related.tsx +66 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +13 -0
- package/blocks/lists/block.json +39 -1
- package/blocks/lists/components/add-to-list-button.tsx +3 -2
- package/blocks/lists/components/list-picker.tsx +5 -1
- package/blocks/lists/components/list-table.tsx +11 -5
- package/blocks/lists/components/lists-screen.tsx +344 -0
- package/blocks/lists/components/suppression-manager.tsx +6 -2
- package/blocks/lists/components/suppressions-screen.tsx +255 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +65 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
- package/blocks/mailboxes/components/domain-detail.tsx +142 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
- package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
- package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
- package/blocks/mailboxes/components/warmup-health.tsx +4 -2
- package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
- package/blocks/newsletter/README.md +13 -0
- package/blocks/newsletter/block.json +47 -23
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
- package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
- package/blocks/newsletter/components/subscriber-table.tsx +12 -6
- package/blocks/nurture/README.md +12 -0
- package/blocks/nurture/block.json +35 -13
- package/blocks/nurture/components/nurture-builder.tsx +3 -1
- package/blocks/nurture/components/nurture-enroll.tsx +6 -3
- package/blocks/nurture/components/nurture-form.tsx +23 -3
- package/blocks/nurture/components/nurture-screen.tsx +283 -0
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +17 -0
- package/blocks/objects/block.json +29 -16
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +2 -1
- package/blocks/objects/components/object-schema-editor.tsx +3 -1
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +12 -0
- package/blocks/pages/block.json +35 -22
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +46 -11
- package/blocks/pages/components/page-list.tsx +6 -3
- package/blocks/pages/components/pages-screen.tsx +154 -0
- package/blocks/quotes/README.md +17 -0
- package/blocks/quotes/block.json +38 -9
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-list.tsx +7 -3
- package/blocks/quotes/components/quote-related.tsx +123 -0
- package/blocks/quotes/components/quotes-screen.tsx +169 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +49 -9
- package/blocks/sequencer/block.json +99 -19
- package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
- package/blocks/sequencer/components/sequence-builder.tsx +211 -46
- package/blocks/sequencer/components/sequence-detail.tsx +259 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
- package/blocks/sequencer/components/sequence-list.tsx +33 -8
- package/blocks/sequencer/components/sequence-stats.tsx +3 -2
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +159 -0
- package/blocks/sequencer/components/step-editor.tsx +3 -1
- package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +11 -0
- package/blocks/social-ads/block.json +101 -2
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
- package/blocks/social-ads/components/audience-sync.tsx +11 -7
- package/blocks/social-ads/components/campaign-detail.tsx +133 -0
- package/blocks/social-ads/components/creative-detail.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +15 -5
- package/blocks/social-ads/components/post-composer.tsx +3 -1
- package/blocks/social-ads/components/post-detail.tsx +160 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +160 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +3 -1
- package/blocks/visitors/components/visitor-company-card.tsx +2 -2
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +11 -2
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +7 -4
- package/blocks/webchat/components/conversation-list.tsx +7 -3
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
- package/blocks/workbench/components/workbench-grid.tsx +9 -3
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +192 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +3 -1
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +2785 -1637
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +113 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +25 -8
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +20 -0
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +32 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense.json +41 -16
- package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
- package/stacks/apollo.json +118 -36
- package/stacks/calendly.json +60 -0
- package/stacks/clay/pages/companies.tsx.tmpl +15 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
- package/stacks/clay.json +59 -26
- package/stacks/gong.json +81 -35
- package/stacks/hootsuite.json +99 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot.json +167 -66
- package/stacks/intercom.json +48 -20
- package/stacks/mailchimp.json +52 -33
- package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
- package/stacks/outreach.json +88 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive.json +115 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce.json +81 -31
- package/stacks/stack.schema.json +6 -1
- package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +102 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +55 -8
- package/templates/next-app/app/globals.css.tmpl +105 -0
- package/templates/next-app/app/layout.tsx.tmpl +18 -4
- package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
- package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
- package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
- package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/lib/nav.ts.tmpl +1 -1
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -14,35 +14,40 @@
|
|
|
14
14
|
"mailboxes"
|
|
15
15
|
],
|
|
16
16
|
"operations": [
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"pause_sequence_sequences__sequence_id__pause_post",
|
|
21
|
-
"resume_sequence_sequences__sequence_id__resume_post",
|
|
22
|
-
"duplicate_sequence_sequences__sequence_id__duplicate_post",
|
|
17
|
+
"add_contacts_to_sequence_sequences__sequence_id__contacts_post",
|
|
18
|
+
"add_sequence_channels_sequences__sequence_id__channels_post",
|
|
19
|
+
"add_sequence_steps_sequences__sequence_id__steps_post",
|
|
23
20
|
"archive_sequence_sequences__sequence_id__archive_post",
|
|
24
|
-
"list_mailboxes_mailboxes_get",
|
|
25
|
-
"list_lists_lists_get",
|
|
26
21
|
"create_sequence_sequences_post",
|
|
27
|
-
"list_sequence_steps_sequences__sequence_id__steps_get",
|
|
28
|
-
"get_sequence_sequences__sequence_id__get",
|
|
29
|
-
"add_sequence_steps_sequences__sequence_id__steps_post",
|
|
30
|
-
"update_sequence_step_sequences__sequence_id__steps__step_id__patch",
|
|
31
22
|
"delete_sequence_step_sequences__sequence_id__steps__step_id__delete",
|
|
23
|
+
"duplicate_sequence_sequences__sequence_id__duplicate_post",
|
|
32
24
|
"get_outreach_connection_integrations_linkedin_outreach_connection_get",
|
|
33
25
|
"get_safe_to_send_deliverability_safe_to_send_get",
|
|
34
|
-
"add_contacts_to_sequence_sequences__sequence_id__contacts_post",
|
|
35
|
-
"get_sequence_stats_sequences__sequence_id__stats_get",
|
|
36
26
|
"get_sequence_analytics_sequences__sequence_id__analytics_get",
|
|
27
|
+
"get_sequence_sequences__sequence_id__get",
|
|
28
|
+
"get_sequence_stats_sequences__sequence_id__stats_get",
|
|
29
|
+
"list_contacts_contacts_get",
|
|
30
|
+
"list_linkedin_accounts_sequencer_channels_linkedin_accounts_get",
|
|
31
|
+
"list_lists_lists_get",
|
|
32
|
+
"list_mailboxes_mailboxes_get",
|
|
33
|
+
"list_sequence_channels_sequences__sequence_id__channels_get",
|
|
37
34
|
"list_sequence_contacts_sequences__sequence_id__contacts_get",
|
|
35
|
+
"list_sequence_steps_sequences__sequence_id__steps_get",
|
|
36
|
+
"list_sequencer_phone_numbers_sequencer_channels_phone_numbers_get",
|
|
37
|
+
"list_sequences_sequences_get",
|
|
38
38
|
"pause_sequence_contact_sequences__sequence_id__contacts__contact_id__pause_post",
|
|
39
|
-
"
|
|
39
|
+
"pause_sequence_sequences__sequence_id__pause_post",
|
|
40
|
+
"preview_sequence_sequences__sequence_id__preview_get",
|
|
41
|
+
"resume_sequence_contact_sequences__sequence_id__contacts__contact_id__resume_post",
|
|
42
|
+
"resume_sequence_sequences__sequence_id__resume_post",
|
|
43
|
+
"run_sequence_sequences__sequence_id__run_post",
|
|
44
|
+
"update_sequence_step_sequences__sequence_id__steps__step_id__patch"
|
|
40
45
|
],
|
|
41
46
|
"components": [
|
|
42
47
|
{
|
|
43
48
|
"name": "SequenceList",
|
|
44
49
|
"file": "components/sequence-list.tsx",
|
|
45
|
-
"purpose": "All sequences with status; launch (after a copy
|
|
50
|
+
"purpose": "All sequences with status; launch (after a preview that checks every step's copy and sender; a launch that would 400 is refused with the reasons), pause, resume, duplicate and archive, each confirmed with the recipient count.",
|
|
46
51
|
"operations": [
|
|
47
52
|
"list_sequences_sequences_get",
|
|
48
53
|
"preview_sequence_sequences__sequence_id__preview_get",
|
|
@@ -62,7 +67,7 @@
|
|
|
62
67
|
{
|
|
63
68
|
"name": "SequenceBuilder",
|
|
64
69
|
"file": "components/sequence-builder.tsx",
|
|
65
|
-
"purpose": "Build a draft with email, LinkedIn, call and call-task steps and a sender, or
|
|
70
|
+
"purpose": "Build a draft with email, LinkedIn, AI call and call-task steps, a required audience list and a sender for every step type (mailbox, Voice AI or dialer number, LinkedIn account), or edit an existing sequence's steps and attach the senders new steps need.",
|
|
66
71
|
"operations": [
|
|
67
72
|
"list_mailboxes_mailboxes_get",
|
|
68
73
|
"list_lists_lists_get",
|
|
@@ -71,7 +76,11 @@
|
|
|
71
76
|
"get_sequence_sequences__sequence_id__get",
|
|
72
77
|
"add_sequence_steps_sequences__sequence_id__steps_post",
|
|
73
78
|
"update_sequence_step_sequences__sequence_id__steps__step_id__patch",
|
|
74
|
-
"delete_sequence_step_sequences__sequence_id__steps__step_id__delete"
|
|
79
|
+
"delete_sequence_step_sequences__sequence_id__steps__step_id__delete",
|
|
80
|
+
"list_sequencer_phone_numbers_sequencer_channels_phone_numbers_get",
|
|
81
|
+
"list_linkedin_accounts_sequencer_channels_linkedin_accounts_get",
|
|
82
|
+
"list_sequence_channels_sequences__sequence_id__channels_get",
|
|
83
|
+
"add_sequence_channels_sequences__sequence_id__channels_post"
|
|
75
84
|
],
|
|
76
85
|
"tiers": [
|
|
77
86
|
"read",
|
|
@@ -121,6 +130,65 @@
|
|
|
121
130
|
"write",
|
|
122
131
|
"external"
|
|
123
132
|
]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "SequencesScreen",
|
|
136
|
+
"file": "components/sequences-screen.tsx",
|
|
137
|
+
"purpose": "Finished sequences page: status stats and filter, list with launch/pause/resume, new-sequence sheet, detail with results, steps and enroll-contacts.",
|
|
138
|
+
"operations": [
|
|
139
|
+
"list_sequences_sequences_get"
|
|
140
|
+
],
|
|
141
|
+
"tiers": [
|
|
142
|
+
"read"
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"name": "SequenceDetail",
|
|
147
|
+
"file": "components/sequence-detail.tsx",
|
|
148
|
+
"purpose": "One sequence, complete: every field, tabs for steps, contacts and results; launch (refused with the reasons when a step has no sender or the copy would block it), pause, resume, duplicate, archive and enroll (confirmed).",
|
|
149
|
+
"operations": [
|
|
150
|
+
"get_sequence_sequences__sequence_id__get",
|
|
151
|
+
"preview_sequence_sequences__sequence_id__preview_get",
|
|
152
|
+
"run_sequence_sequences__sequence_id__run_post",
|
|
153
|
+
"pause_sequence_sequences__sequence_id__pause_post",
|
|
154
|
+
"resume_sequence_sequences__sequence_id__resume_post",
|
|
155
|
+
"duplicate_sequence_sequences__sequence_id__duplicate_post",
|
|
156
|
+
"archive_sequence_sequences__sequence_id__archive_post",
|
|
157
|
+
"list_contacts_contacts_get"
|
|
158
|
+
],
|
|
159
|
+
"tiers": [
|
|
160
|
+
"read",
|
|
161
|
+
"write",
|
|
162
|
+
"external",
|
|
163
|
+
"destructive"
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "SequenceStepsPanel",
|
|
168
|
+
"file": "components/sequence-steps-panel.tsx",
|
|
169
|
+
"purpose": "The steps of a sequence as records: channel, wait, progress; open one for every field and counts, then edit.",
|
|
170
|
+
"operations": [
|
|
171
|
+
"list_sequence_steps_sequences__sequence_id__steps_get",
|
|
172
|
+
"get_sequence_stats_sequences__sequence_id__stats_get"
|
|
173
|
+
],
|
|
174
|
+
"tiers": [
|
|
175
|
+
"read"
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"name": "SequenceEnrollments",
|
|
180
|
+
"file": "components/sequence-enrollments.tsx",
|
|
181
|
+
"purpose": "Enrolled contacts of a sequence as records: state, step, the contact itself; pause directly, resume confirmed.",
|
|
182
|
+
"operations": [
|
|
183
|
+
"list_sequence_contacts_sequences__sequence_id__contacts_get",
|
|
184
|
+
"pause_sequence_contact_sequences__sequence_id__contacts__contact_id__pause_post",
|
|
185
|
+
"resume_sequence_contact_sequences__sequence_id__contacts__contact_id__resume_post"
|
|
186
|
+
],
|
|
187
|
+
"tiers": [
|
|
188
|
+
"read",
|
|
189
|
+
"write",
|
|
190
|
+
"external"
|
|
191
|
+
]
|
|
124
192
|
}
|
|
125
193
|
],
|
|
126
194
|
"hooks": [],
|
|
@@ -174,6 +242,7 @@
|
|
|
174
242
|
],
|
|
175
243
|
"scopes": [
|
|
176
244
|
"campaigns:read",
|
|
245
|
+
"contacts:read",
|
|
177
246
|
"integrations:read",
|
|
178
247
|
"lists:read",
|
|
179
248
|
"sequences:delete",
|
|
@@ -206,9 +275,20 @@
|
|
|
206
275
|
"decision": "Call steps and do-not-call numbers",
|
|
207
276
|
"default": "Phone steps never dial do-not-call numbers",
|
|
208
277
|
"askHuman": false
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"decision": "Which senders a sequence gets",
|
|
281
|
+
"default": "One per step type the steps use: the picked mailbox (channel type from its source; Microsoft direct mailboxes are not offered), a Voice AI number for AI calls, a dialer number for call tasks, a LinkedIn account. The draft is not created without them",
|
|
282
|
+
"askHuman": false
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"decision": "Audience list",
|
|
286
|
+
"default": "Required when creating a draft (the API cannot set it later, and a sequence without one cannot launch)",
|
|
287
|
+
"askHuman": false
|
|
209
288
|
}
|
|
210
289
|
],
|
|
211
290
|
"peerDependencies": {
|
|
212
|
-
"react": ">=18"
|
|
291
|
+
"react": ">=18",
|
|
292
|
+
"lucide-react": ">=0.400.0"
|
|
213
293
|
}
|
|
214
294
|
}
|
|
@@ -18,12 +18,14 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { useRef } from "react";
|
|
21
|
+
import type { ReactNode } from "react";
|
|
21
22
|
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
22
23
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
23
24
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
24
25
|
import { ApiError } from "@/g8/components/api-error";
|
|
25
26
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
26
27
|
import { newIdempotencyKey, objectOf } from "../lib/sequencer-utils";
|
|
28
|
+
import { pluralWord } from "@/g8/lib/plural";
|
|
27
29
|
|
|
28
30
|
export interface EnrollDialogProps {
|
|
29
31
|
open: boolean;
|
|
@@ -35,6 +37,8 @@ export interface EnrollDialogProps {
|
|
|
35
37
|
listId?: number;
|
|
36
38
|
onClose: () => void;
|
|
37
39
|
onEnrolled?: (result: { contactsAffected: number }) => void;
|
|
40
|
+
/** Extra line shown in the confirm (for example that only part of a large list is included). */
|
|
41
|
+
note?: ReactNode;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
function Verdict() {
|
|
@@ -48,10 +52,11 @@ function Verdict() {
|
|
|
48
52
|
);
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
export function EnrollDialog({ open, sequenceId, sequenceName, contactIds, listId, onClose, onEnrolled }: EnrollDialogProps) {
|
|
55
|
+
export function EnrollDialog({ open, sequenceId, sequenceName, contactIds, listId, onClose, onEnrolled, note }: EnrollDialogProps) {
|
|
52
56
|
const key = useRef<{ sig: string; value: string } | null>(null);
|
|
53
57
|
const sequence = useG8Query((signal) => g8Get(`/sequences/${sequenceId}`, undefined, { signal }), [sequenceId], {
|
|
54
58
|
enabled: open && listId == null,
|
|
59
|
+
key: ["sequences", "detail", sequenceId],
|
|
55
60
|
});
|
|
56
61
|
const resolvedList = listId ?? (objectOf(sequence.data).associated_list_id as number | undefined);
|
|
57
62
|
|
|
@@ -62,7 +67,7 @@ export function EnrollDialog({ open, sequenceId, sequenceName, contactIds, listI
|
|
|
62
67
|
const res = objectOf(await g8Post(`/sequences/${sequenceId}/contacts`, body, { idempotencyKey: key.current.value }));
|
|
63
68
|
onEnrolled?.({ contactsAffected: Number(res.contacts_affected ?? 0) });
|
|
64
69
|
onClose();
|
|
65
|
-
});
|
|
70
|
+
}, { invalidates: [["sequences", "contacts", sequenceId], ["sequences", "stats", sequenceId]] });
|
|
66
71
|
|
|
67
72
|
if (!open) return null;
|
|
68
73
|
const missingList = listId == null && !sequence.loading && resolvedList == null;
|
|
@@ -70,10 +75,11 @@ export function EnrollDialog({ open, sequenceId, sequenceName, contactIds, listI
|
|
|
70
75
|
<ConfirmSpendDialog
|
|
71
76
|
open
|
|
72
77
|
tier="external"
|
|
73
|
-
title={`Enroll ${contactIds.length.toLocaleString()}
|
|
78
|
+
title={`Enroll ${contactIds.length.toLocaleString()} ${pluralWord(contactIds.length, "contact")} in "${sequenceName ?? sequenceId}"`}
|
|
74
79
|
description={
|
|
75
80
|
<div>
|
|
76
81
|
<p>They start receiving this sequence's steps. Contacts already enrolled are skipped.</p>
|
|
82
|
+
{note ? <div className="mt-2">{note}</div> : null}
|
|
77
83
|
{missingList ? (
|
|
78
84
|
<p role="alert" className="mt-2 text-red-700">
|
|
79
85
|
This sequence has no audience list. Pass the list these contacts came from as `listId`.
|
|
@@ -6,17 +6,26 @@
|
|
|
6
6
|
* is launched (SequenceList does that).
|
|
7
7
|
*
|
|
8
8
|
* SDK operations, in order:
|
|
9
|
-
* create: 1. listMailboxes GET /mailboxes (read; the sender)
|
|
9
|
+
* create: 1. listMailboxes GET /mailboxes (read; the email sender, with `source`)
|
|
10
10
|
* 2. listLists GET /lists (read; the audience, becomes associated_list_id)
|
|
11
|
-
* 3.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* 3. listSequencerPhoneNumbers GET /sequencer/channels/phone_numbers (read; only with call steps)
|
|
12
|
+
* listLinkedinAccounts GET /sequencer/channels/linkedin_accounts (read; only with LinkedIn steps)
|
|
13
|
+
* 4. createSequence POST /sequences (external tier: confirmed; steps + senders in ONE call)
|
|
14
|
+
* edit: 1. getSequence / listSequenceSteps / listSequenceChannels GET /sequences/{id}, /steps, /channels (read)
|
|
15
|
+
* 2. addSequenceChannels POST /sequences/{id}/channels (write; a sender new steps need, if picked)
|
|
16
|
+
* 3. addSequenceSteps POST /sequences/{id}/steps (write; appended steps)
|
|
17
|
+
* 4. updateSequenceStep PATCH /sequences/{id}/steps/{step_id} (external: confirmed; replaces step_data)
|
|
18
|
+
* 5. deleteSequenceStep DELETE /sequences/{id}/steps/{step_id} (destructive: confirmed)
|
|
16
19
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
+
* Every step type needs its own sender or POST /sequences/{id}/run refuses the launch with 400: email steps a
|
|
21
|
+
* mailbox, AI call steps a Voice AI number, call tasks a dialer number, LinkedIn steps a LinkedIn account. The
|
|
22
|
+
* builder asks for each one the steps need and will not create the draft without them. The audience list is
|
|
23
|
+
* required too: the API has no way to set it later, and a sequence without one cannot launch.
|
|
24
|
+
*
|
|
25
|
+
* Ids passed between steps: the chosen mailbox `id` + `email` (channel type from its `source`), phone `id` +
|
|
26
|
+
* `phone_number`, LinkedIn account `id` become the sequence channels; the list `id` becomes `associated_list_id`;
|
|
27
|
+
* createSequence returns the new sequence `id` (handed to `onSaved`). Changing a LIVE sequence's copy changes
|
|
28
|
+
* what real people receive, which is why the update dialog says so.
|
|
20
29
|
*/
|
|
21
30
|
|
|
22
31
|
import { useRef, useState } from "react";
|
|
@@ -29,19 +38,23 @@ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
|
29
38
|
import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
|
|
30
39
|
import { StepEditor } from "./step-editor";
|
|
31
40
|
import {
|
|
32
|
-
channelTypeFor,
|
|
33
41
|
fromApiStep,
|
|
34
42
|
KIND_LABEL,
|
|
35
43
|
KINDS,
|
|
36
44
|
listOf,
|
|
45
|
+
mailboxChannelType,
|
|
46
|
+
mailboxUnsupportedReason,
|
|
37
47
|
newIdempotencyKey,
|
|
38
48
|
newStep,
|
|
39
49
|
objectOf,
|
|
50
|
+
requiredSenders,
|
|
40
51
|
statusGroup,
|
|
41
52
|
stepIssues,
|
|
42
53
|
toApiStep,
|
|
43
54
|
} from "../lib/sequencer-utils";
|
|
44
55
|
import type { ApiStep, DraftStep, StepKind } from "../lib/sequencer-utils";
|
|
56
|
+
import { pluralWord } from "@/g8/lib/plural";
|
|
57
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
45
58
|
|
|
46
59
|
export interface SequenceBuilderProps {
|
|
47
60
|
/** Edit this sequence's steps. Omit to build a new draft. */
|
|
@@ -57,7 +70,38 @@ interface MailboxOption {
|
|
|
57
70
|
id: string;
|
|
58
71
|
email: string | null;
|
|
59
72
|
provider?: string | null;
|
|
73
|
+
source?: string | null;
|
|
74
|
+
}
|
|
75
|
+
interface PhoneOption {
|
|
76
|
+
id: number | string;
|
|
77
|
+
phone_number: string;
|
|
78
|
+
/** "ai" (Voice AI) or "dialer"; the API matches AI call steps to "ai" and call tasks to "dialer". */
|
|
79
|
+
inbound_type?: string | null;
|
|
80
|
+
}
|
|
81
|
+
interface LinkedinOption {
|
|
82
|
+
id: number | string;
|
|
83
|
+
name?: string | null;
|
|
84
|
+
display_name?: string | null;
|
|
85
|
+
public_identifier?: string | null;
|
|
86
|
+
account_id?: string | null;
|
|
87
|
+
email?: string | null;
|
|
88
|
+
}
|
|
89
|
+
interface SenderChannel {
|
|
90
|
+
channel_id: number;
|
|
91
|
+
channel_value: string;
|
|
92
|
+
channel_type: string;
|
|
60
93
|
}
|
|
94
|
+
|
|
95
|
+
/** Every bucket of a picker response (available, active, drafted), each id once. */
|
|
96
|
+
function bucketed<T extends { id: number | string }>(envelope: unknown, keys: string[]): T[] {
|
|
97
|
+
const data = objectOf(envelope);
|
|
98
|
+
const all = keys.flatMap((k) => (Array.isArray(data[k]) ? (data[k] as T[]) : []));
|
|
99
|
+
return all.filter((item, i) => all.findIndex((o) => String(o.id) === String(item.id)) === i);
|
|
100
|
+
}
|
|
101
|
+
const PHONE_KEYS = ["phone_numbers", "active_phone_numbers", "drafted_phone_numbers"];
|
|
102
|
+
const LINKEDIN_KEYS = ["linkedin_accounts", "active_linkedin_accounts", "drafted_linkedin_accounts"];
|
|
103
|
+
const phoneFits = (p: PhoneOption, type: "ai" | "dialer") => !p.inbound_type || p.inbound_type === type;
|
|
104
|
+
const linkedinLabel = (a: LinkedinOption) => a.name || a.display_name || a.public_identifier || a.email || `Account ${a.id}`;
|
|
61
105
|
interface ListOption {
|
|
62
106
|
id: number;
|
|
63
107
|
title: string;
|
|
@@ -70,7 +114,7 @@ function describe(p: Pending, live: boolean): { tier: ConfirmTier; title: string
|
|
|
70
114
|
return {
|
|
71
115
|
tier: "external",
|
|
72
116
|
title: "Create this sequence as a draft",
|
|
73
|
-
description: "Saves the steps and sender. Nobody is contacted until you launch it.",
|
|
117
|
+
description: "Saves the steps, the audience list and a sender for every step type. Nobody is contacted until you launch it.",
|
|
74
118
|
label: "Create draft",
|
|
75
119
|
};
|
|
76
120
|
if (p.kind === "update")
|
|
@@ -91,9 +135,13 @@ function describe(p: Pending, live: boolean): { tier: ConfirmTier; title: string
|
|
|
91
135
|
}
|
|
92
136
|
|
|
93
137
|
export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved, className }: SequenceBuilderProps) {
|
|
138
|
+
const brand = useBrand();
|
|
94
139
|
const editing = Boolean(sequenceId);
|
|
95
140
|
const [name, setName] = useState("");
|
|
96
141
|
const [mailboxId, setMailboxId] = useState("");
|
|
142
|
+
const [aiPhoneId, setAiPhoneId] = useState("");
|
|
143
|
+
const [dialerPhoneId, setDialerPhoneId] = useState("");
|
|
144
|
+
const [linkedinId, setLinkedinId] = useState("");
|
|
97
145
|
const [listId, setListId] = useState(defaultListId ? String(defaultListId) : "");
|
|
98
146
|
const [steps, setSteps] = useState<DraftStep[]>([]);
|
|
99
147
|
const [loadedFor, setLoadedFor] = useState<string | null>(null);
|
|
@@ -106,6 +154,8 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
|
|
|
106
154
|
if (keys.current[slot]?.sig !== sig) keys.current[slot] = { sig, value: newIdempotencyKey() };
|
|
107
155
|
return keys.current[slot].value;
|
|
108
156
|
};
|
|
157
|
+
// Senders already attached in edit mode by an earlier (failed) append: never attach them twice.
|
|
158
|
+
const attached = useRef<Set<string>>(new Set());
|
|
109
159
|
|
|
110
160
|
const mailboxes = useG8Query((signal) => g8Get("/mailboxes", { limit: 200 }, { signal }), [], { enabled: !editing });
|
|
111
161
|
const lists = useG8Query((signal) => g8Get("/lists", { limit: 100 }, { signal }), [], { enabled: !editing });
|
|
@@ -120,24 +170,71 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
|
|
|
120
170
|
[sequenceId],
|
|
121
171
|
{ enabled: editing },
|
|
122
172
|
);
|
|
173
|
+
// Which senders the sequence already has (edit mode). Unknown when this read fails: nothing is then asked for.
|
|
174
|
+
const current = useG8Query(
|
|
175
|
+
async (signal) => listOf<{ channel_type?: string | null }>(await g8Get(`/sequences/${sequenceId}/channels`, undefined, { signal })),
|
|
176
|
+
[sequenceId],
|
|
177
|
+
{ enabled: editing },
|
|
178
|
+
);
|
|
123
179
|
if (editing && existing.data && loadedFor !== sequenceId) {
|
|
124
180
|
setLoadedFor(sequenceId ?? null);
|
|
125
181
|
setSteps(existing.data.steps);
|
|
126
182
|
}
|
|
127
183
|
const live = statusGroup(existing.data?.status) === "live";
|
|
128
184
|
|
|
185
|
+
// The senders the steps being written need. In edit mode only new steps count, minus what is attached.
|
|
186
|
+
const fresh = steps.filter((s) => !s.id);
|
|
187
|
+
const has = new Set((current.data ?? []).map((c) => (c.channel_type ?? "").toUpperCase()));
|
|
188
|
+
const want = requiredSenders((editing ? fresh : steps).map((s) => s.kind));
|
|
189
|
+
const need = editing
|
|
190
|
+
? {
|
|
191
|
+
// Mailboxes are attached with the sequence; edit mode has no mailbox picker (the launch check names the gap).
|
|
192
|
+
email: false,
|
|
193
|
+
aiPhone: want.aiPhone && Boolean(current.data) && !has.has("PHONE"),
|
|
194
|
+
dialerPhone: want.dialerPhone && Boolean(current.data) && !has.has("PHONE"),
|
|
195
|
+
linkedin: want.linkedin && Boolean(current.data) && !has.has("LINKEDIN") && !has.has("LINKEDIN_NETRION"),
|
|
196
|
+
}
|
|
197
|
+
: want;
|
|
198
|
+
const phones = useG8Query((signal) => g8Get("/sequencer/channels/phone_numbers", undefined, { signal }), [], {
|
|
199
|
+
enabled: need.aiPhone || need.dialerPhone,
|
|
200
|
+
});
|
|
201
|
+
const linkedin = useG8Query((signal) => g8Get("/sequencer/channels/linkedin_accounts", undefined, { signal }), [], {
|
|
202
|
+
enabled: need.linkedin,
|
|
203
|
+
});
|
|
204
|
+
const provider = String(objectOf(linkedin.data).provider ?? "HEYREACH").toUpperCase();
|
|
205
|
+
const linkedinStepType = provider === "NETRION" ? "NETRION" : "HEYREACH";
|
|
206
|
+
const phoneOptions = bucketed<PhoneOption>(phones.data, PHONE_KEYS);
|
|
207
|
+
const linkedinOptions = bucketed<LinkedinOption>(linkedin.data, LINKEDIN_KEYS);
|
|
208
|
+
|
|
209
|
+
/** The sender channels picked for what `need` asks, in the shape POST /sequences and /channels take. */
|
|
210
|
+
const senderChannels = (): SenderChannel[] => {
|
|
211
|
+
const out: SenderChannel[] = [];
|
|
212
|
+
const mailbox = listOf<MailboxOption>(mailboxes.data).find((m) => m.id === mailboxId);
|
|
213
|
+
const type = mailbox ? mailboxChannelType(mailbox) : null;
|
|
214
|
+
if (mailbox && type) out.push({ channel_id: Number(mailbox.id), channel_value: mailbox.email ?? "", channel_type: type });
|
|
215
|
+
for (const id of new Set([need.aiPhone ? aiPhoneId : "", need.dialerPhone ? dialerPhoneId : ""].filter(Boolean))) {
|
|
216
|
+
const phone = phoneOptions.find((p) => String(p.id) === id);
|
|
217
|
+
if (phone) out.push({ channel_id: Number(phone.id), channel_value: phone.phone_number, channel_type: "PHONE" });
|
|
218
|
+
}
|
|
219
|
+
const account = need.linkedin ? linkedinOptions.find((a) => String(a.id) === linkedinId) : undefined;
|
|
220
|
+
if (account)
|
|
221
|
+
out.push({
|
|
222
|
+
channel_id: Number(account.id),
|
|
223
|
+
channel_value: provider === "NETRION" ? (account.public_identifier ?? account.display_name ?? account.account_id ?? "") : linkedinLabel(account),
|
|
224
|
+
channel_type: provider === "NETRION" ? "LINKEDIN_NETRION" : "LINKEDIN",
|
|
225
|
+
});
|
|
226
|
+
return out;
|
|
227
|
+
};
|
|
228
|
+
|
|
129
229
|
const run = useG8Mutation(async (p: Pending) => {
|
|
130
230
|
if (p.kind === "create") {
|
|
131
|
-
const mailbox = listOf<MailboxOption>(mailboxes.data).find((m) => m.id === mailboxId);
|
|
132
231
|
const body = {
|
|
133
232
|
name: name.trim(),
|
|
134
233
|
user_email: ownerEmail,
|
|
135
234
|
associated_list_id: listId ? Number(listId) : undefined,
|
|
136
235
|
finish_on_reply: true,
|
|
137
|
-
steps: steps.map((s, i) => toApiStep(s, i + 1)),
|
|
138
|
-
channels:
|
|
139
|
-
? [{ channel_id: Number(mailbox.id), channel_value: mailbox.email ?? "", channel_type: channelTypeFor(mailbox.provider) }]
|
|
140
|
-
: [],
|
|
236
|
+
steps: steps.map((s, i) => toApiStep(s, i + 1, linkedinStepType)),
|
|
237
|
+
channels: senderChannels(),
|
|
141
238
|
};
|
|
142
239
|
const res = await g8Post<{ id: string }>("/sequences", body, { idempotencyKey: keyFor("create", body) });
|
|
143
240
|
setSaved(`Draft "${body.name}" created. Launch it from the sequence list.`);
|
|
@@ -157,9 +254,22 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
|
|
|
157
254
|
}
|
|
158
255
|
});
|
|
159
256
|
const append = useG8Mutation(async () => {
|
|
257
|
+
// Attach the senders the new steps need first (each one once, even across retries), then the steps.
|
|
258
|
+
const channels = senderChannels().filter((c) => !attached.current.has(`${c.channel_type}:${c.channel_id}`));
|
|
259
|
+
if (channels.length > 0) {
|
|
260
|
+
await g8Post(`/sequences/${sequenceId}/channels`, { channels });
|
|
261
|
+
channels.forEach((c) => attached.current.add(`${c.channel_type}:${c.channel_id}`));
|
|
262
|
+
void current.refetch();
|
|
263
|
+
}
|
|
160
264
|
const fresh = steps.filter((s) => !s.id);
|
|
161
265
|
const offset = steps.length - fresh.length;
|
|
162
|
-
|
|
266
|
+
// New LinkedIn steps follow the sequence's own provider: an existing LinkedIn step's type, else the LinkedIn
|
|
267
|
+
// sender already attached, else the org's provider. A HEYREACH step next to a LINKEDIN_NETRION sender
|
|
268
|
+
// (or the reverse) could never launch.
|
|
269
|
+
const existingLinkedin = steps.find((s) => s.id && s.kind === "linkedin")?.step_type;
|
|
270
|
+
const sequenceLinkedin =
|
|
271
|
+
existingLinkedin ?? (has.has("LINKEDIN_NETRION") ? "NETRION" : has.has("LINKEDIN") ? "HEYREACH" : linkedinStepType);
|
|
272
|
+
const body = { steps: fresh.map((s, i) => toApiStep(s, offset + i + 1, sequenceLinkedin)) };
|
|
163
273
|
await g8Post(`/sequences/${sequenceId}/steps`, body, { idempotencyKey: keyFor("append", body) });
|
|
164
274
|
// The steps are saved: drop the unsaved copies at once so a second click cannot append them again,
|
|
165
275
|
// then reload the real list (with ids) instead of trusting the response shape.
|
|
@@ -179,38 +289,42 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
|
|
|
179
289
|
return next;
|
|
180
290
|
});
|
|
181
291
|
const invalid = steps.some((s) => stepIssues(s).length > 0);
|
|
182
|
-
|
|
183
|
-
const
|
|
292
|
+
// What is still missing before the draft can be created (shown under the button, so it is never a mystery).
|
|
293
|
+
const missing = [
|
|
294
|
+
!name.trim() && "a name",
|
|
295
|
+
!listId && "an audience list",
|
|
296
|
+
steps.length === 0 && "a step",
|
|
297
|
+
need.email && !mailboxId && "a mailbox for the email steps",
|
|
298
|
+
need.aiPhone && !aiPhoneId && "a Voice AI number for the AI call steps",
|
|
299
|
+
need.dialerPhone && !dialerPhoneId && "a dialer number for the call tasks",
|
|
300
|
+
need.linkedin && !linkedinId && "a LinkedIn account for the LinkedIn steps",
|
|
301
|
+
].filter(Boolean) as string[];
|
|
302
|
+
const canCreate = missing.length === 0 && !invalid;
|
|
184
303
|
|
|
185
304
|
if (editing && existing.error) return <ApiError error={existing.error} onRetry={() => void existing.refetch()} className={className} />;
|
|
186
305
|
if (editing && existing.loading && !existing.data) return <Skeleton className={`h-48 w-full ${className ?? ""}`} />;
|
|
187
306
|
const copy = pending ? describe(pending, live) : null;
|
|
307
|
+
const select = "h-9 rounded-md border border-border bg-background px-2 text-sm";
|
|
188
308
|
|
|
189
309
|
return (
|
|
190
310
|
<div className={`space-y-4 ${className ?? ""}`}>
|
|
191
311
|
{!editing ? (
|
|
192
312
|
<div className="grid gap-2 sm:grid-cols-3">
|
|
193
313
|
<Input aria-label="Sequence name" placeholder="Q4 outbound" value={name} onChange={(e) => setName(e.target.value)} />
|
|
194
|
-
<select
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
))}
|
|
314
|
+
<select aria-label="Send from" className={select} value={mailboxId} onChange={(e) => setMailboxId(e.target.value)}>
|
|
315
|
+
<option value="">{steps.length > 0 && !want.email ? "Mailbox (no email steps)" : "Send from..."}</option>
|
|
316
|
+
{listOf<MailboxOption>(mailboxes.data).map((m) => {
|
|
317
|
+
const reason = mailboxUnsupportedReason(m);
|
|
318
|
+
return (
|
|
319
|
+
<option key={m.id} value={m.id} disabled={Boolean(reason)} title={reason ?? undefined}>
|
|
320
|
+
{m.email}
|
|
321
|
+
{reason ? ` (${reason})` : ""}
|
|
322
|
+
</option>
|
|
323
|
+
);
|
|
324
|
+
})}
|
|
206
325
|
</select>
|
|
207
|
-
<select
|
|
208
|
-
|
|
209
|
-
className="h-9 rounded-md border border-gray-300 px-2 text-sm"
|
|
210
|
-
value={listId}
|
|
211
|
-
onChange={(e) => setListId(e.target.value)}
|
|
212
|
-
>
|
|
213
|
-
<option value="">Audience list (optional)</option>
|
|
326
|
+
<select aria-label="Audience list" className={select} value={listId} onChange={(e) => setListId(e.target.value)}>
|
|
327
|
+
<option value="">Audience list (required)</option>
|
|
214
328
|
{listOf<ListOption>(lists.data).map((l) => (
|
|
215
329
|
<option key={l.id} value={l.id}>
|
|
216
330
|
{l.title}
|
|
@@ -220,12 +334,53 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
|
|
|
220
334
|
</select>
|
|
221
335
|
</div>
|
|
222
336
|
) : null}
|
|
223
|
-
{
|
|
337
|
+
{need.aiPhone || need.dialerPhone || need.linkedin ? (
|
|
338
|
+
<div className="grid gap-2 sm:grid-cols-3">
|
|
339
|
+
{need.aiPhone ? (
|
|
340
|
+
<select aria-label="AI call number" className={select} value={aiPhoneId} onChange={(e) => setAiPhoneId(e.target.value)}>
|
|
341
|
+
<option value="">{phones.loading ? "Loading numbers..." : "Voice AI number for AI calls..."}</option>
|
|
342
|
+
{phoneOptions.filter((p) => phoneFits(p, "ai")).map((p) => (
|
|
343
|
+
<option key={p.id} value={String(p.id)}>
|
|
344
|
+
{p.phone_number}
|
|
345
|
+
</option>
|
|
346
|
+
))}
|
|
347
|
+
</select>
|
|
348
|
+
) : null}
|
|
349
|
+
{need.dialerPhone ? (
|
|
350
|
+
<select aria-label="Dialer number" className={select} value={dialerPhoneId} onChange={(e) => setDialerPhoneId(e.target.value)}>
|
|
351
|
+
<option value="">{phones.loading ? "Loading numbers..." : "Dialer number for call tasks..."}</option>
|
|
352
|
+
{phoneOptions.filter((p) => phoneFits(p, "dialer")).map((p) => (
|
|
353
|
+
<option key={p.id} value={String(p.id)}>
|
|
354
|
+
{p.phone_number}
|
|
355
|
+
</option>
|
|
356
|
+
))}
|
|
357
|
+
</select>
|
|
358
|
+
) : null}
|
|
359
|
+
{need.linkedin ? (
|
|
360
|
+
<select aria-label="LinkedIn account" className={select} value={linkedinId} onChange={(e) => setLinkedinId(e.target.value)}>
|
|
361
|
+
<option value="">{linkedin.loading ? "Loading accounts..." : "LinkedIn account to send from..."}</option>
|
|
362
|
+
{linkedinOptions.map((a) => (
|
|
363
|
+
<option key={a.id} value={String(a.id)}>
|
|
364
|
+
{linkedinLabel(a)}
|
|
365
|
+
</option>
|
|
366
|
+
))}
|
|
367
|
+
</select>
|
|
368
|
+
) : null}
|
|
369
|
+
</div>
|
|
370
|
+
) : null}
|
|
371
|
+
{need.linkedin && linkedin.data && linkedinOptions.length === 0 ? (
|
|
372
|
+
<p className="text-sm text-muted-foreground">
|
|
373
|
+
{String(objectOf(linkedin.data).message ?? "No LinkedIn account is connected.")} Connect one in the {brand.linkLabel} app.
|
|
374
|
+
</p>
|
|
375
|
+
) : null}
|
|
376
|
+
{mailboxes.error || lists.error || phones.error || linkedin.error ? (
|
|
224
377
|
<ApiError
|
|
225
|
-
error={mailboxes.error ?? lists.error}
|
|
378
|
+
error={mailboxes.error ?? lists.error ?? phones.error ?? linkedin.error}
|
|
226
379
|
onRetry={() => {
|
|
227
380
|
void mailboxes.refetch();
|
|
228
381
|
void lists.refetch();
|
|
382
|
+
if (need.aiPhone || need.dialerPhone) void phones.refetch();
|
|
383
|
+
if (need.linkedin) void linkedin.refetch();
|
|
229
384
|
}}
|
|
230
385
|
/>
|
|
231
386
|
) : null}
|
|
@@ -258,13 +413,13 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
|
|
|
258
413
|
</div>
|
|
259
414
|
</div>
|
|
260
415
|
))}
|
|
261
|
-
{steps.length === 0 ? <p className="text-sm text-
|
|
416
|
+
{steps.length === 0 ? <p className="text-sm text-muted-foreground">No steps yet. Add the first touch below.</p> : null}
|
|
262
417
|
</div>
|
|
263
418
|
|
|
264
419
|
<div className="flex flex-wrap items-center gap-2">
|
|
265
420
|
{KINDS.map((k) => (
|
|
266
421
|
<Button key={k} size="sm" variant="outline" onClick={() => add(k)}>
|
|
267
|
-
Add {KIND_LABEL[k].toLowerCase()}
|
|
422
|
+
Add {KIND_LABEL[k].split(" ").map((w) => (/^[A-Z]{2,}$/.test(w) ? w : w.toLowerCase())).join(" ")}
|
|
268
423
|
</Button>
|
|
269
424
|
))}
|
|
270
425
|
</div>
|
|
@@ -274,13 +429,23 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
|
|
|
274
429
|
disabled={fresh.length === 0 || fresh.some((s) => stepIssues(s).length > 0) || append.loading}
|
|
275
430
|
onClick={() => void append.mutate(undefined)}
|
|
276
431
|
>
|
|
277
|
-
Add {fresh.length} new
|
|
432
|
+
Add {fresh.length} new {pluralWord(fresh.length, "step")}
|
|
278
433
|
</Button>
|
|
279
434
|
) : (
|
|
280
|
-
<
|
|
281
|
-
|
|
282
|
-
|
|
435
|
+
<div className="space-y-1">
|
|
436
|
+
<Button disabled={!canCreate} onClick={() => setPending({ kind: "create" })}>
|
|
437
|
+
Create draft sequence
|
|
438
|
+
</Button>
|
|
439
|
+
{missing.length > 0 && (name.trim() || steps.length > 0) ? (
|
|
440
|
+
<p className="text-xs text-muted-foreground">Still needed: {missing.join(", ")}.</p>
|
|
441
|
+
) : null}
|
|
442
|
+
</div>
|
|
283
443
|
)}
|
|
444
|
+
{editing && (need.aiPhone || need.dialerPhone || need.linkedin) ? (
|
|
445
|
+
<p className="text-xs text-muted-foreground">
|
|
446
|
+
This sequence has no sender for the new steps yet. Pick one above, or it cannot launch.
|
|
447
|
+
</p>
|
|
448
|
+
) : null}
|
|
284
449
|
{run.error ? <ApiError error={run.error} onRetry={run.reset} /> : null}
|
|
285
450
|
{append.error ? <ApiError error={append.error} onRetry={append.reset} /> : null}
|
|
286
451
|
{saved ? (
|