@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.
Files changed (388) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +172 -1
  3. package/blocks/_foundation/block.json +234 -13
  4. package/blocks/_foundation/components/api-error.tsx +71 -25
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  7. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  8. package/blocks/_foundation/components/empty-state.tsx +39 -7
  9. package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
  10. package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
  11. package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
  12. package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
  13. package/blocks/_foundation/components/screen/index.ts +22 -0
  14. package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
  17. package/blocks/_foundation/components/screen/section-card.tsx +47 -0
  18. package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
  19. package/blocks/_foundation/components/screen/split-view.tsx +125 -0
  20. package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
  21. package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
  22. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  23. package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
  24. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  25. package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
  26. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  27. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  28. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  29. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  30. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  31. package/blocks/_foundation/hooks/use-url-state.ts +74 -0
  32. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  33. package/blocks/_foundation/lib/g8-client.ts +1 -1
  34. package/blocks/_foundation/lib/plural.ts +23 -0
  35. package/blocks/_foundation/server/connect-route.ts +15 -0
  36. package/blocks/_foundation/server/connect.ts +127 -0
  37. package/blocks/_foundation/server/local-key.ts +70 -0
  38. package/blocks/_foundation/server/proxy.ts +103 -3
  39. package/blocks/_foundation/server/route.ts +16 -4
  40. package/blocks/agents/README.md +14 -0
  41. package/blocks/agents/block.json +33 -1
  42. package/blocks/agents/components/agent-builder.tsx +13 -8
  43. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  44. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  45. package/blocks/agents/components/agent-runs.tsx +16 -7
  46. package/blocks/agents/components/agents-screen.tsx +184 -0
  47. package/blocks/analytics/README.md +13 -0
  48. package/blocks/analytics/block.json +45 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  50. package/blocks/analytics/components/leaderboard.tsx +14 -7
  51. package/blocks/analytics/components/report-builder.tsx +23 -9
  52. package/blocks/analytics/components/report-detail.tsx +84 -0
  53. package/blocks/analytics/components/trend-chart.tsx +62 -0
  54. package/blocks/appointments/README.md +21 -1
  55. package/blocks/appointments/block.json +43 -2
  56. package/blocks/appointments/components/availability-editor.tsx +17 -5
  57. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  58. package/blocks/appointments/components/event-type-related.tsx +77 -0
  59. package/blocks/appointments/components/meeting-list.tsx +14 -6
  60. package/blocks/appointments/components/meetings-screen.tsx +157 -0
  61. package/blocks/appointments/components/scheduling-screen.tsx +122 -0
  62. package/blocks/appointments/hooks/use-event-types.ts +17 -0
  63. package/blocks/block.schema.json +18 -0
  64. package/blocks/block.types.ts +26 -0
  65. package/blocks/campaigns/README.md +41 -11
  66. package/blocks/campaigns/block.json +105 -42
  67. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  68. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  69. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  70. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  71. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  72. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  73. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  74. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  75. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  76. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  77. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  78. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  79. package/blocks/coach/README.md +10 -0
  80. package/blocks/coach/block.json +16 -1
  81. package/blocks/coach/components/coach-screen.tsx +222 -0
  82. package/blocks/copilot/README.md +13 -0
  83. package/blocks/copilot/block.json +14 -1
  84. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  85. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  87. package/blocks/crm/README.md +28 -0
  88. package/blocks/crm/block.json +58 -2
  89. package/blocks/crm/components/companies-screen.tsx +345 -0
  90. package/blocks/crm/components/company-detail.tsx +8 -6
  91. package/blocks/crm/components/company-table.tsx +11 -4
  92. package/blocks/crm/components/contact-detail.tsx +6 -3
  93. package/blocks/crm/components/contact-table.tsx +10 -3
  94. package/blocks/crm/components/contacts-screen.tsx +359 -0
  95. package/blocks/crm/components/dedupe-review.tsx +7 -5
  96. package/blocks/crm/components/import-wizard.tsx +4 -2
  97. package/blocks/crm/components/notes-panel.tsx +4 -4
  98. package/blocks/crm/components/record-form.tsx +5 -3
  99. package/blocks/crm/components/task-list.tsx +4 -4
  100. package/blocks/crm/components/tasks-screen.tsx +379 -0
  101. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  102. package/blocks/crm/hooks/use-page-query.ts +7 -1
  103. package/blocks/crm/lib/crm-types.ts +12 -2
  104. package/blocks/crm/lib/screen-parts.tsx +105 -0
  105. package/blocks/deals/README.md +34 -3
  106. package/blocks/deals/block.json +45 -4
  107. package/blocks/deals/components/deal-detail.tsx +27 -13
  108. package/blocks/deals/components/deal-related.tsx +87 -0
  109. package/blocks/deals/components/deals-screen.tsx +298 -0
  110. package/blocks/deals/components/pipeline-board.tsx +4 -2
  111. package/blocks/deals/components/stage-editor.tsx +4 -4
  112. package/blocks/deals/hooks/use-default-owner.ts +60 -0
  113. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  114. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  115. package/blocks/deals/lib/deals-types.ts +14 -0
  116. package/blocks/dialer/README.md +34 -2
  117. package/blocks/dialer/block.json +67 -1
  118. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  119. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  120. package/blocks/dialer/components/call-log.tsx +7 -3
  121. package/blocks/dialer/components/call-queue.tsx +1 -1
  122. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  123. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  124. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  125. package/blocks/dialer/components/session-start-form.tsx +4 -2
  126. package/blocks/dialer/components/softphone.tsx +3 -1
  127. package/blocks/enrichment/README.md +14 -2
  128. package/blocks/enrichment/block.json +32 -4
  129. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  130. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  131. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  132. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  133. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  134. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  135. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  136. package/blocks/forms/README.md +12 -0
  137. package/blocks/forms/block.json +39 -17
  138. package/blocks/forms/components/form-builder.tsx +3 -1
  139. package/blocks/forms/components/form-embed.tsx +3 -1
  140. package/blocks/forms/components/forms-screen.tsx +353 -0
  141. package/blocks/forms/components/submissions-table.tsx +16 -6
  142. package/blocks/inbox/README.md +27 -3
  143. package/blocks/inbox/block.json +14 -2
  144. package/blocks/inbox/components/inbox-list.tsx +7 -3
  145. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  146. package/blocks/inbox/components/reply-composer.tsx +65 -9
  147. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  148. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  149. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  150. package/blocks/inbox/lib/inbox-types.ts +47 -1
  151. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  152. package/blocks/integrations/README.md +9 -0
  153. package/blocks/integrations/block.json +46 -1
  154. package/blocks/integrations/components/crm-detail.tsx +100 -0
  155. package/blocks/integrations/components/integration-list.tsx +9 -4
  156. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  157. package/blocks/integrations/components/sync-status.tsx +5 -3
  158. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  159. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  160. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  161. package/blocks/intent/README.md +14 -0
  162. package/blocks/intent/block.json +27 -1
  163. package/blocks/intent/components/intent-feed.tsx +5 -2
  164. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  165. package/blocks/intent/components/intent-screen.tsx +265 -0
  166. package/blocks/intent/components/keyword-detail.tsx +124 -0
  167. package/blocks/lifecycle/README.md +16 -0
  168. package/blocks/lifecycle/block.json +34 -7
  169. package/blocks/lifecycle/components/account-related.tsx +66 -0
  170. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  171. package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
  172. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  173. package/blocks/lists/README.md +13 -0
  174. package/blocks/lists/block.json +39 -1
  175. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  176. package/blocks/lists/components/list-picker.tsx +5 -1
  177. package/blocks/lists/components/list-table.tsx +11 -5
  178. package/blocks/lists/components/lists-screen.tsx +344 -0
  179. package/blocks/lists/components/suppression-manager.tsx +6 -2
  180. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  181. package/blocks/mailboxes/README.md +17 -0
  182. package/blocks/mailboxes/block.json +65 -17
  183. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  184. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  185. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  186. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  187. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  188. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  189. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  190. package/blocks/newsletter/README.md +13 -0
  191. package/blocks/newsletter/block.json +47 -23
  192. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  193. package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
  194. package/blocks/newsletter/components/subscriber-table.tsx +12 -6
  195. package/blocks/nurture/README.md +12 -0
  196. package/blocks/nurture/block.json +35 -13
  197. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  198. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  199. package/blocks/nurture/components/nurture-form.tsx +23 -3
  200. package/blocks/nurture/components/nurture-screen.tsx +283 -0
  201. package/blocks/nurture/lib/nurture-types.ts +1 -1
  202. package/blocks/objects/README.md +17 -0
  203. package/blocks/objects/block.json +29 -16
  204. package/blocks/objects/components/object-not-found.tsx +3 -1
  205. package/blocks/objects/components/object-record-detail.tsx +2 -1
  206. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  207. package/blocks/objects/components/object-table.tsx +13 -3
  208. package/blocks/objects/components/objects-screen.tsx +147 -0
  209. package/blocks/objects/lib/object-types.ts +1 -1
  210. package/blocks/pages/README.md +12 -0
  211. package/blocks/pages/block.json +35 -22
  212. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  213. package/blocks/pages/components/content-library.tsx +46 -11
  214. package/blocks/pages/components/page-list.tsx +6 -3
  215. package/blocks/pages/components/pages-screen.tsx +154 -0
  216. package/blocks/quotes/README.md +17 -0
  217. package/blocks/quotes/block.json +38 -9
  218. package/blocks/quotes/components/product-picker.tsx +3 -1
  219. package/blocks/quotes/components/quote-list.tsx +7 -3
  220. package/blocks/quotes/components/quote-related.tsx +123 -0
  221. package/blocks/quotes/components/quotes-screen.tsx +169 -0
  222. package/blocks/radar/README.md +10 -0
  223. package/blocks/radar/block.json +41 -1
  224. package/blocks/radar/components/competitor-detail.tsx +117 -0
  225. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  226. package/blocks/radar/components/radar-report.tsx +4 -2
  227. package/blocks/radar/components/radar-screen.tsx +214 -0
  228. package/blocks/radar/components/report-detail.tsx +104 -0
  229. package/blocks/search/README.md +10 -0
  230. package/blocks/search/block.json +16 -1
  231. package/blocks/search/components/filter-builder.tsx +3 -3
  232. package/blocks/search/components/prospect-results.tsx +5 -3
  233. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  234. package/blocks/search/components/saved-searches.tsx +4 -1
  235. package/blocks/search/hooks/use-filter-options.ts +20 -6
  236. package/blocks/search/lib/search-types.ts +2 -0
  237. package/blocks/sequencer/README.md +49 -9
  238. package/blocks/sequencer/block.json +99 -19
  239. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  240. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  241. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  242. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  243. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  244. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  245. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  246. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  247. package/blocks/sequencer/components/step-editor.tsx +3 -1
  248. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  249. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  250. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  251. package/blocks/social-ads/README.md +11 -0
  252. package/blocks/social-ads/block.json +101 -2
  253. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  254. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  255. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  256. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  257. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  258. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  259. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  260. package/blocks/social-ads/components/post-composer.tsx +3 -1
  261. package/blocks/social-ads/components/post-detail.tsx +160 -0
  262. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  263. package/blocks/social-ads/components/social-screen.tsx +160 -0
  264. package/blocks/visitors/README.md +10 -0
  265. package/blocks/visitors/block.json +25 -1
  266. package/blocks/visitors/components/snippet-install.tsx +3 -1
  267. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  268. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  269. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  270. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  271. package/blocks/webchat/README.md +10 -0
  272. package/blocks/webchat/block.json +21 -7
  273. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  274. package/blocks/webchat/components/conversation-list.tsx +7 -3
  275. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  276. package/blocks/workbench/README.md +11 -0
  277. package/blocks/workbench/block.json +48 -1
  278. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  279. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  280. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  281. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  282. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  283. package/blocks/workflows/README.md +13 -0
  284. package/blocks/workflows/block.json +28 -9
  285. package/blocks/workflows/components/run-history.tsx +31 -9
  286. package/blocks/workflows/components/trigger-config.tsx +3 -1
  287. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  288. package/blocks/workflows/components/workflow-list.tsx +13 -6
  289. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  290. package/dist/cli.js +2785 -1637
  291. package/dist/errors.d.mts +34 -0
  292. package/dist/errors.d.ts +34 -0
  293. package/dist/errors.js +46 -0
  294. package/dist/errors.js.map +1 -0
  295. package/dist/errors.mjs +21 -0
  296. package/dist/errors.mjs.map +1 -0
  297. package/dist/index.browser.js +11 -5
  298. package/dist/index.browser.js.map +1 -1
  299. package/dist/index.browser.mjs +11 -5
  300. package/dist/index.browser.mjs.map +1 -1
  301. package/dist/index.d.mts +164 -25
  302. package/dist/index.d.ts +164 -25
  303. package/dist/index.js +11 -5
  304. package/dist/index.js.map +1 -1
  305. package/dist/index.mjs +11 -5
  306. package/dist/index.mjs.map +1 -1
  307. package/dist/react.browser.js +10 -4
  308. package/dist/react.browser.js.map +1 -1
  309. package/dist/react.browser.mjs +10 -4
  310. package/dist/react.browser.mjs.map +1 -1
  311. package/dist/react.d.mts +142 -5
  312. package/dist/react.d.ts +142 -5
  313. package/dist/react.js +10 -4
  314. package/dist/react.js.map +1 -1
  315. package/dist/react.mjs +10 -4
  316. package/dist/react.mjs.map +1 -1
  317. package/dist/types/blocks/add.d.ts +11 -4
  318. package/dist/types/blocks/brand.d.ts +113 -0
  319. package/dist/types/blocks/compose.d.ts +53 -0
  320. package/dist/types/blocks/create.d.ts +25 -8
  321. package/dist/types/blocks/doctor.d.ts +6 -1
  322. package/dist/types/blocks/flows.d.ts +20 -0
  323. package/dist/types/blocks/navfile.d.ts +35 -0
  324. package/dist/types/blocks/paths.d.ts +4 -0
  325. package/dist/types/blocks/prefixes.d.ts +37 -0
  326. package/dist/types/blocks/registry.d.ts +32 -0
  327. package/dist/types/blocks/screens.d.ts +53 -0
  328. package/dist/types/cli.d.ts +2 -0
  329. package/dist/types/errors.d.ts +33 -0
  330. package/dist/types/http.d.ts +2 -18
  331. package/dist/types/version.d.ts +2 -2
  332. package/docs/agents.md +15 -4
  333. package/docs/api/README.md +2 -2
  334. package/docs/api/by-path.md +1 -0
  335. package/docs/api/desk.d.ts +98 -2
  336. package/docs/api/desk.md +123 -1
  337. package/docs/api/inbox.d.ts +10 -2
  338. package/docs/api/inbox.md +10 -2
  339. package/docs/api/seo.d.ts +19 -1
  340. package/docs/api/seo.md +24 -1
  341. package/package.json +16 -2
  342. package/stacks/6sense.json +41 -16
  343. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  344. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  345. package/stacks/apollo.json +118 -36
  346. package/stacks/calendly.json +60 -0
  347. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  348. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  349. package/stacks/clay.json +59 -26
  350. package/stacks/gong.json +81 -35
  351. package/stacks/hootsuite.json +99 -0
  352. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  353. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  354. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  355. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  356. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  357. package/stacks/hubspot.json +167 -66
  358. package/stacks/intercom.json +48 -20
  359. package/stacks/mailchimp.json +52 -33
  360. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  361. package/stacks/outreach.json +88 -25
  362. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  363. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  364. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  365. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  366. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  367. package/stacks/pipedrive.json +115 -0
  368. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  369. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  370. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  371. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  372. package/stacks/salesforce.json +81 -31
  373. package/stacks/stack.schema.json +6 -1
  374. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  375. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  376. package/stacks/zoominfo.json +102 -0
  377. package/styles/styles.css +1 -1
  378. package/templates/next-app/README.md.tmpl +55 -8
  379. package/templates/next-app/app/globals.css.tmpl +105 -0
  380. package/templates/next-app/app/layout.tsx.tmpl +18 -4
  381. package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
  382. package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
  383. package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
  384. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
  385. package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
  386. package/templates/next-app/dot-gitignore.tmpl +2 -0
  387. package/templates/next-app/lib/nav.ts.tmpl +1 -1
  388. package/templates/next-app/package.json.tmpl +1 -1
@@ -14,35 +14,40 @@
14
14
  "mailboxes"
15
15
  ],
16
16
  "operations": [
17
- "list_sequences_sequences_get",
18
- "preview_sequence_sequences__sequence_id__preview_get",
19
- "run_sequence_sequences__sequence_id__run_post",
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
- "resume_sequence_contact_sequences__sequence_id__contacts__contact_id__resume_post"
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 preview), pause, resume, duplicate and archive, each confirmed with the recipient count.",
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 edit the steps of an existing sequence.",
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()} contact${contactIds.length === 1 ? "" : "s"} in "${sequenceName ?? sequenceId}"`}
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. createSequence POST /sequences (external tier: confirmed; steps + sender in ONE call)
12
- * edit: 1. getSequence / listSequenceSteps GET /sequences/{id}, /sequences/{id}/steps (read)
13
- * 2. addSequenceSteps POST /sequences/{id}/steps (write; appended steps)
14
- * 3. updateSequenceStep PATCH /sequences/{id}/steps/{step_id} (external: confirmed; replaces step_data)
15
- * 4. deleteSequenceStep DELETE /sequences/{id}/steps/{step_id} (destructive: confirmed)
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
- * Ids passed between steps: the chosen mailbox `id` + `email` become the sequence channel; the list `id` becomes
18
- * `associated_list_id`; createSequence returns the new sequence `id` (handed to `onSaved`). Changing a LIVE
19
- * sequence's copy changes what real people receive, which is why the update dialog says so.
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: mailbox
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
- const body = { steps: fresh.map((s, i) => toApiStep(s, offset + i + 1)) };
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
- const canCreate = name.trim() && steps.length > 0 && !invalid && mailboxId;
183
- const fresh = steps.filter((s) => !s.id);
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
- aria-label="Send from"
196
- className="h-9 rounded-md border border-gray-300 px-2 text-sm"
197
- value={mailboxId}
198
- onChange={(e) => setMailboxId(e.target.value)}
199
- >
200
- <option value="">Send from...</option>
201
- {listOf<MailboxOption>(mailboxes.data).map((m) => (
202
- <option key={m.id} value={m.id}>
203
- {m.email}
204
- </option>
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
- aria-label="Audience list"
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
- {mailboxes.error || lists.error ? (
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-gray-500">No steps yet. Add the first touch below.</p> : null}
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 step{fresh.length === 1 ? "" : "s"}
432
+ Add {fresh.length} new {pluralWord(fresh.length, "step")}
278
433
  </Button>
279
434
  ) : (
280
- <Button disabled={!canCreate} onClick={() => setPending({ kind: "create" })}>
281
- Create draft sequence
282
- </Button>
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 ? (