@graph8/sdk 0.370.0 → 0.374.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 (646) hide show
  1. package/README.md +22 -3
  2. package/blocks/_foundation/README.md +61 -0
  3. package/blocks/_foundation/block.json +57 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  6. package/blocks/_foundation/components/empty-state.tsx +5 -2
  7. package/blocks/_foundation/components/list-select.tsx +112 -0
  8. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  9. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  10. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  11. package/blocks/_foundation/components/screen/index.ts +1 -0
  12. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  13. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  14. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  15. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  16. package/blocks/_foundation/lib/paging.ts +57 -0
  17. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  18. package/blocks/_foundation/server/auth.ts +815 -0
  19. package/blocks/agents/README.md +81 -4
  20. package/blocks/agents/block.json +311 -2
  21. package/blocks/agents/components/agent-activity.tsx +112 -0
  22. package/blocks/agents/components/agent-approvals.tsx +135 -0
  23. package/blocks/agents/components/agent-builder.tsx +17 -3
  24. package/blocks/agents/components/agent-conversations.tsx +143 -0
  25. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  26. package/blocks/agents/components/agent-operators.tsx +164 -0
  27. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  28. package/blocks/agents/components/agent-runs.tsx +3 -3
  29. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  30. package/blocks/agents/components/agent-test-history.tsx +121 -0
  31. package/blocks/agents/components/agents-screen.tsx +53 -11
  32. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  33. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  34. package/blocks/agents/components/from-template-button.tsx +65 -0
  35. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  36. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  37. package/blocks/agents/components/knowledge-form.tsx +69 -0
  38. package/blocks/agents/components/name-dialog.tsx +32 -0
  39. package/blocks/agents/components/receptionist-button.tsx +46 -0
  40. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  41. package/blocks/agents/components/skill-editor.tsx +147 -0
  42. package/blocks/agents/components/skill-library.tsx +111 -0
  43. package/blocks/agents/lib/agent-types.ts +7 -1
  44. package/blocks/agents/lib/facts.tsx +13 -0
  45. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  46. package/blocks/agents/lib/load-more.tsx +141 -0
  47. package/blocks/agents/lib/skill-utils.ts +91 -0
  48. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  50. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  51. package/blocks/analytics/components/report-builder.tsx +15 -2
  52. package/blocks/appointments/README.md +6 -0
  53. package/blocks/appointments/block.json +27 -0
  54. package/blocks/appointments/components/booking-page.tsx +2 -2
  55. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  56. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  57. package/blocks/appointments/components/meeting-list.tsx +29 -13
  58. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  59. package/blocks/campaigns/README.md +1 -1
  60. package/blocks/campaigns/block.json +34 -1
  61. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  62. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  63. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  64. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  65. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  66. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  67. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  68. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  69. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  70. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  71. package/blocks/coach/components/call-scorecard.tsx +7 -7
  72. package/blocks/company-intelligence/README.md +132 -0
  73. package/blocks/company-intelligence/block.json +318 -0
  74. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  75. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  76. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  77. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  78. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  79. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  80. package/blocks/company-intelligence/components/research-run.tsx +165 -0
  81. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  82. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  83. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  84. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  85. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  86. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  87. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  88. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  89. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  90. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  91. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  92. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  93. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  94. package/blocks/crm/README.md +25 -0
  95. package/blocks/crm/block.json +257 -9
  96. package/blocks/crm/components/bulk-update.tsx +119 -0
  97. package/blocks/crm/components/companies-screen.tsx +23 -9
  98. package/blocks/crm/components/company-detail.tsx +2 -2
  99. package/blocks/crm/components/contact-detail.tsx +4 -4
  100. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  101. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  102. package/blocks/crm/components/contact-lookup-link.tsx +85 -0
  103. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  104. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  105. package/blocks/crm/components/contacts-screen.tsx +77 -18
  106. package/blocks/crm/components/dedupe-review.tsx +6 -1
  107. package/blocks/crm/components/import-wizard.tsx +3 -7
  108. package/blocks/crm/components/list-select.tsx +20 -0
  109. package/blocks/crm/components/owner-assign.tsx +132 -0
  110. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  111. package/blocks/crm/components/record-form.tsx +1 -1
  112. package/blocks/crm/components/record-open-links.tsx +79 -0
  113. package/blocks/crm/components/tag-assign.tsx +112 -0
  114. package/blocks/crm/components/tag-manager.tsx +178 -0
  115. package/blocks/crm/components/tasks-screen.tsx +35 -15
  116. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  117. package/blocks/crm/components/verify-email-button.tsx +93 -0
  118. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  119. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  120. package/blocks/crm/hooks/use-page-query.ts +3 -1
  121. package/blocks/crm/lib/crm-types.ts +14 -0
  122. package/blocks/crm/lib/read-lists.ts +16 -0
  123. package/blocks/crm/lib/record-actions.ts +102 -0
  124. package/blocks/deals/README.md +25 -6
  125. package/blocks/deals/block.json +168 -2
  126. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  127. package/blocks/deals/components/create-deal-button.tsx +109 -0
  128. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  129. package/blocks/deals/components/deal-detail.tsx +7 -5
  130. package/blocks/deals/components/deal-filters.tsx +65 -0
  131. package/blocks/deals/components/deal-line-items.tsx +120 -0
  132. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  133. package/blocks/deals/components/deal-related.tsx +30 -9
  134. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  135. package/blocks/deals/components/deals-screen.tsx +57 -22
  136. package/blocks/deals/components/forecast-view.tsx +4 -4
  137. package/blocks/deals/components/pipeline-board.tsx +8 -6
  138. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  139. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  140. package/blocks/deals/components/stage-editor.tsx +1 -1
  141. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  142. package/blocks/deals/lib/deal-extras.ts +156 -0
  143. package/blocks/dialer/README.md +14 -5
  144. package/blocks/dialer/block.json +17 -2
  145. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  146. package/blocks/dialer/components/call-log-screen.tsx +10 -2
  147. package/blocks/dialer/components/call-log.tsx +16 -4
  148. package/blocks/dialer/components/call-queue.tsx +3 -3
  149. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  150. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  151. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  152. package/blocks/dialer/components/session-start-form.tsx +58 -11
  153. package/blocks/dialer/components/softphone.tsx +1 -1
  154. package/blocks/documents/README.md +86 -0
  155. package/blocks/documents/block.json +378 -0
  156. package/blocks/documents/components/account-bar.tsx +48 -0
  157. package/blocks/documents/components/correspondence-form.tsx +95 -0
  158. package/blocks/documents/components/correspondence-import.tsx +106 -0
  159. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  160. package/blocks/documents/components/documents-screen.tsx +76 -0
  161. package/blocks/documents/components/library-document-detail.tsx +175 -0
  162. package/blocks/documents/components/library-document-form.tsx +105 -0
  163. package/blocks/documents/components/library-folder-form.tsx +74 -0
  164. package/blocks/documents/components/library-panel.tsx +235 -0
  165. package/blocks/documents/components/library-upload.tsx +160 -0
  166. package/blocks/documents/components/question-row.tsx +130 -0
  167. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  168. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  169. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  170. package/blocks/documents/lib/documents-types.ts +252 -0
  171. package/blocks/documents/lib/download.ts +64 -0
  172. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  173. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  174. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  175. package/blocks/email-signatures/README.md +123 -0
  176. package/blocks/email-signatures/block.json +341 -0
  177. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  178. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  179. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  180. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  181. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  182. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  183. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  184. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  185. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  186. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  187. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  188. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  189. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  190. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  191. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  192. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  193. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  194. package/blocks/enrichment/README.md +38 -10
  195. package/blocks/enrichment/block.json +164 -12
  196. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  197. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  198. package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
  199. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  200. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  201. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  202. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  203. package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
  204. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  205. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  206. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  207. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  208. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  209. package/blocks/enrichment/lib/enrichment-types.ts +51 -0
  210. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  211. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  212. package/blocks/forms/README.md +10 -0
  213. package/blocks/forms/block.json +107 -0
  214. package/blocks/forms/components/form-activity.tsx +71 -0
  215. package/blocks/forms/components/form-backfill.tsx +108 -0
  216. package/blocks/forms/components/form-builder.tsx +1 -1
  217. package/blocks/forms/components/form-embed.tsx +1 -1
  218. package/blocks/forms/components/form-groups.tsx +151 -0
  219. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  220. package/blocks/forms/components/form-route-history.tsx +72 -0
  221. package/blocks/forms/components/form-saved-views.tsx +81 -0
  222. package/blocks/forms/components/forms-screen.tsx +57 -20
  223. package/blocks/forms/components/more-rows.tsx +30 -0
  224. package/blocks/forms/components/submissions-export.tsx +67 -0
  225. package/blocks/forms/components/submissions-table.tsx +15 -6
  226. package/blocks/forms/lib/forms-extra.ts +67 -0
  227. package/blocks/inbox/README.md +45 -4
  228. package/blocks/inbox/block.json +145 -8
  229. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  230. package/blocks/inbox/components/compose-message.tsx +193 -0
  231. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  232. package/blocks/inbox/components/inbox-search.tsx +106 -0
  233. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  234. package/blocks/inbox/components/tag-manager.tsx +136 -0
  235. package/blocks/inbox/components/thread-actions.tsx +133 -0
  236. package/blocks/inbox/components/thread-badge.tsx +2 -2
  237. package/blocks/inbox/components/thread-view.tsx +3 -1
  238. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  239. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  240. package/blocks/integrations/README.md +54 -1
  241. package/blocks/integrations/block.json +189 -15
  242. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  243. package/blocks/integrations/components/connection-health.tsx +59 -0
  244. package/blocks/integrations/components/crm-detail.tsx +20 -1
  245. package/blocks/integrations/components/crm-records.tsx +103 -0
  246. package/blocks/integrations/components/field-mappings.tsx +61 -0
  247. package/blocks/integrations/components/integration-list.tsx +4 -4
  248. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  249. package/blocks/integrations/components/migration-panel.tsx +133 -0
  250. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  251. package/blocks/integrations/components/sync-status.tsx +6 -6
  252. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  253. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  254. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  255. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  256. package/blocks/integrations/lib/crm-utils.ts +39 -0
  257. package/blocks/integrations/lib/facts.tsx +13 -0
  258. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  259. package/blocks/integrations/lib/load-more.tsx +141 -0
  260. package/blocks/intent/components/account-intent-score.tsx +5 -5
  261. package/blocks/intent/components/intent-feed.tsx +8 -8
  262. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  263. package/blocks/intent/lib/intent-utils.ts +4 -4
  264. package/blocks/lifecycle/components/account-related.tsx +8 -1
  265. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  266. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  267. package/blocks/lists/README.md +7 -1
  268. package/blocks/lists/block.json +31 -3
  269. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  270. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  271. package/blocks/lists/components/list-picker.tsx +13 -3
  272. package/blocks/lists/components/list-table.tsx +1 -1
  273. package/blocks/lists/components/lists-screen.tsx +4 -2
  274. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  275. package/blocks/lists/components/suppression-manager.tsx +27 -6
  276. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  277. package/blocks/mailboxes/block.json +4 -0
  278. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  279. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  280. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  281. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  282. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  283. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  284. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  285. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  286. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  287. package/blocks/newsletter/README.md +12 -0
  288. package/blocks/newsletter/block.json +117 -0
  289. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  290. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  291. package/blocks/newsletter/components/more-rows.tsx +30 -0
  292. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  293. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  294. package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
  295. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  296. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  297. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  298. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  299. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  300. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  301. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  302. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  303. package/blocks/nurture/components/nurture-form.tsx +5 -8
  304. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  305. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  306. package/blocks/objects/README.md +1 -1
  307. package/blocks/objects/block.json +13 -5
  308. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  309. package/blocks/objects/components/object-record-detail.tsx +8 -1
  310. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  311. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  312. package/blocks/pages/README.md +14 -0
  313. package/blocks/pages/block.json +123 -2
  314. package/blocks/pages/components/more-rows.tsx +30 -0
  315. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  316. package/blocks/pages/components/page-analytics.tsx +176 -0
  317. package/blocks/pages/components/page-clone.tsx +99 -0
  318. package/blocks/pages/components/page-domains.tsx +167 -0
  319. package/blocks/pages/components/page-editor.tsx +1 -1
  320. package/blocks/pages/components/page-generation-status.tsx +48 -0
  321. package/blocks/pages/components/page-tracking.tsx +137 -0
  322. package/blocks/pages/components/pages-screen.tsx +57 -16
  323. package/blocks/pages/components/seo-report.tsx +6 -1
  324. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  325. package/blocks/pages/lib/studio.ts +7 -0
  326. package/blocks/phone-numbers/README.md +74 -0
  327. package/blocks/phone-numbers/block.json +262 -0
  328. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  329. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  330. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  331. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  332. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  333. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  334. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  335. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  336. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  337. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  338. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  339. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  340. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  341. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  342. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  343. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  344. package/blocks/pickers/README.md +23 -0
  345. package/blocks/pickers/block.json +103 -0
  346. package/blocks/pickers/components/company-picker.tsx +90 -0
  347. package/blocks/pickers/components/contact-picker.tsx +131 -0
  348. package/blocks/pickers/components/member-picker.tsx +99 -0
  349. package/blocks/pickers/components/search-picker.tsx +98 -0
  350. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  351. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  352. package/blocks/pickers/lib/use-debounced.ts +13 -0
  353. package/blocks/programmatic-pages/README.md +82 -0
  354. package/blocks/programmatic-pages/block.json +292 -0
  355. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  356. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  357. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  358. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  359. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  360. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  361. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  362. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  363. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  364. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  365. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  366. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  367. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  368. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  369. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  370. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  371. package/blocks/quotes/README.md +38 -1
  372. package/blocks/quotes/block.json +170 -6
  373. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  374. package/blocks/quotes/components/manual-products.tsx +144 -0
  375. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  376. package/blocks/quotes/components/quote-builder.tsx +1 -1
  377. package/blocks/quotes/components/quote-documents.tsx +94 -0
  378. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  379. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  380. package/blocks/quotes/components/quote-list.tsx +28 -8
  381. package/blocks/quotes/components/quote-related.tsx +3 -2
  382. package/blocks/quotes/components/quote-settings.tsx +184 -0
  383. package/blocks/quotes/components/quote-templates.tsx +189 -0
  384. package/blocks/quotes/components/quote-viewer.tsx +23 -5
  385. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  386. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  387. package/blocks/quotes/lib/quote-admin.ts +264 -0
  388. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  389. package/blocks/radar/components/radar-overview.tsx +4 -4
  390. package/blocks/radar/components/radar-screen.tsx +2 -2
  391. package/blocks/sequencer/README.md +61 -0
  392. package/blocks/sequencer/block.json +200 -6
  393. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  394. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  395. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  396. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  397. package/blocks/sequencer/components/sequence-builder.tsx +32 -23
  398. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  399. package/blocks/sequencer/components/sequence-detail.tsx +10 -2
  400. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  401. package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
  402. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  403. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  404. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  405. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  406. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  407. package/blocks/sequencer/components/sequences-screen.tsx +39 -6
  408. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  409. package/blocks/sequencer/components/step-editor.tsx +21 -12
  410. package/blocks/sequencer/components/step-templates.tsx +224 -0
  411. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  412. package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
  413. package/blocks/signals/README.md +39 -0
  414. package/blocks/signals/block.json +287 -0
  415. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  416. package/blocks/signals/components/listener-form.tsx +184 -0
  417. package/blocks/signals/components/matched-posts.tsx +195 -0
  418. package/blocks/signals/components/post-engagements.tsx +114 -0
  419. package/blocks/signals/components/record-social-tab.tsx +49 -0
  420. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  421. package/blocks/signals/components/social-settings.tsx +128 -0
  422. package/blocks/signals/components/source-lists.tsx +83 -0
  423. package/blocks/signals/components/top-engagers.tsx +68 -0
  424. package/blocks/signals/components/x-integration.tsx +129 -0
  425. package/blocks/signals/lib/signals-types.ts +228 -0
  426. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  427. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  428. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  429. package/blocks/social-ads/README.md +8 -0
  430. package/blocks/social-ads/block.json +137 -4
  431. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  432. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  433. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  434. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  435. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  436. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  437. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  438. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  439. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  440. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  441. package/blocks/social-ads/components/post-composer.tsx +4 -2
  442. package/blocks/social-ads/components/post-detail.tsx +3 -0
  443. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  444. package/blocks/social-ads/components/social-performance.tsx +80 -0
  445. package/blocks/social-ads/components/social-screen.tsx +33 -6
  446. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  447. package/blocks/surveys/README.md +91 -0
  448. package/blocks/surveys/block.json +299 -0
  449. package/blocks/surveys/components/account-nps.tsx +69 -0
  450. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  451. package/blocks/surveys/components/org-summary.tsx +82 -0
  452. package/blocks/surveys/components/sending-domains.tsx +113 -0
  453. package/blocks/surveys/components/survey-form.tsx +126 -0
  454. package/blocks/surveys/components/survey-responses.tsx +145 -0
  455. package/blocks/surveys/components/survey-results.tsx +72 -0
  456. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  457. package/blocks/surveys/components/survey-send.tsx +162 -0
  458. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  459. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  460. package/blocks/surveys/lib/surveys-types.ts +228 -0
  461. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  462. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  463. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  464. package/blocks/visitors/components/snippet-install.tsx +18 -9
  465. package/blocks/visitors/components/traffic-chart.tsx +7 -6
  466. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  467. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  468. package/blocks/web-analytics/README.md +102 -0
  469. package/blocks/web-analytics/block.json +292 -0
  470. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  471. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  472. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  473. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  474. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  475. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  476. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  477. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  478. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  479. package/blocks/web-analytics/components/report-card.tsx +71 -0
  480. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  481. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  482. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  483. package/blocks/web-analytics/lib/page-path.ts +14 -0
  484. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  485. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  486. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  487. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  488. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  489. package/blocks/webchat/components/conversation-list.tsx +1 -1
  490. package/blocks/workbench/README.md +54 -2
  491. package/blocks/workbench/block.json +234 -5
  492. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  493. package/blocks/workbench/components/run-progress.tsx +1 -1
  494. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  495. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  496. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  497. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  498. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  499. package/blocks/workbench/components/workbench-grid.tsx +71 -8
  500. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  501. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  502. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  503. package/blocks/workbench/components/workbench-screen.tsx +65 -22
  504. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  505. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  506. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  507. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  508. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  509. package/blocks/workbench/lib/api-status.ts +8 -0
  510. package/blocks/workbench/lib/automation-types.ts +257 -0
  511. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  512. package/dist/cli.js +744 -394
  513. package/dist/index.browser.js +1 -1
  514. package/dist/index.browser.js.map +1 -1
  515. package/dist/index.browser.mjs +1 -1
  516. package/dist/index.browser.mjs.map +1 -1
  517. package/dist/index.d.mts +2 -2
  518. package/dist/index.d.ts +2 -2
  519. package/dist/index.js +1 -1
  520. package/dist/index.js.map +1 -1
  521. package/dist/index.mjs +1 -1
  522. package/dist/index.mjs.map +1 -1
  523. package/dist/react.browser.js +1 -1
  524. package/dist/react.browser.js.map +1 -1
  525. package/dist/react.browser.mjs +1 -1
  526. package/dist/react.browser.mjs.map +1 -1
  527. package/dist/react.d.mts +2 -2
  528. package/dist/react.d.ts +2 -2
  529. package/dist/react.js +1 -1
  530. package/dist/react.js.map +1 -1
  531. package/dist/react.mjs +1 -1
  532. package/dist/react.mjs.map +1 -1
  533. package/dist/types/blocks/auth.d.ts +45 -0
  534. package/dist/types/blocks/brand.d.ts +11 -2
  535. package/dist/types/blocks/create.d.ts +11 -4
  536. package/dist/types/blocks/doctor.d.ts +1 -0
  537. package/dist/types/blocks/flows.d.ts +7 -1
  538. package/dist/types/blocks/orgBrand.d.ts +34 -0
  539. package/dist/types/cli.d.ts +3 -0
  540. package/dist/types/version.d.ts +2 -2
  541. package/docs/api/desk.d.ts +2 -2
  542. package/docs/api/desk.md +3 -3
  543. package/docs/api/forms.md +1 -1
  544. package/package.json +1 -1
  545. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  546. package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
  547. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  548. package/stacks/6sense.json +37 -4
  549. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  550. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  551. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  552. package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
  553. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  554. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  555. package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
  556. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  557. package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
  558. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  559. package/stacks/apollo.json +68 -9
  560. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  561. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  562. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  563. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  564. package/stacks/calendly.json +8 -4
  565. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  566. package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
  567. package/stacks/clay/pages/lists.tsx.tmpl +24 -0
  568. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  569. package/stacks/clay.json +16 -3
  570. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  571. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  572. package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
  573. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  574. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  575. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  576. package/stacks/gong.json +23 -6
  577. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  578. package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
  579. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  580. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  581. package/stacks/hootsuite.json +19 -4
  582. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  583. package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
  584. package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
  585. package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
  586. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  587. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  588. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  589. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  590. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  591. package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
  592. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  593. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  594. package/stacks/hubspot.json +66 -7
  595. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  596. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  597. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  598. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  599. package/stacks/intercom.json +18 -4
  600. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  601. package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
  602. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  603. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  604. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  605. package/stacks/mailchimp.json +22 -6
  606. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  607. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  608. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  609. package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
  610. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  611. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  612. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  613. package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
  614. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  615. package/stacks/outreach.json +38 -9
  616. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  617. package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
  618. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  619. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  620. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  621. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  622. package/stacks/pipedrive.json +14 -2
  623. package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
  624. package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
  625. package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
  626. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  627. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  628. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  629. package/stacks/salesforce.json +17 -3
  630. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  631. package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
  632. package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
  633. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  634. package/stacks/zoominfo.json +36 -3
  635. package/styles/styles.css +1 -1
  636. package/templates/auth/auth-route.ts.tmpl +16 -0
  637. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  638. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  639. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  640. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  641. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  642. package/templates/auth/middleware.ts.tmpl +14 -0
  643. package/templates/next-app/README.md.tmpl +5 -0
  644. package/templates/next-app/app/globals.css.tmpl +349 -64
  645. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  646. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -34,9 +34,13 @@ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
34
34
  import { useG8Query } from "@/g8/hooks/use-g8-query";
35
35
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
36
36
  import { ApiError } from "@/g8/components/api-error";
37
+ import { ListSelect } from "@/g8/crm/components/list-select";
38
+ import { FirstNotice } from "@/g8/components/paging-notices";
37
39
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
38
40
  import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
41
+ import { StepAiAssist } from "./step-ai-assist";
39
42
  import { StepEditor } from "./step-editor";
43
+ import { StepTemplatePicker } from "./step-templates";
40
44
  import {
41
45
  fromApiStep,
42
46
  KIND_LABEL,
@@ -63,6 +67,10 @@ export interface SequenceBuilderProps {
63
67
  /** Optional owner email sent as `user_email` on create. */
64
68
  ownerEmail?: string;
65
69
  onSaved?: (sequenceId: string) => void;
70
+ /** New sequence only: start with this name (for example from a template). */
71
+ initialName?: string;
72
+ /** New sequence only: start with these unsaved steps (for example from a template). Sender and audience are still asked for. */
73
+ initialSteps?: DraftStep[];
66
74
  className?: string;
67
75
  }
68
76
 
@@ -102,11 +110,8 @@ const PHONE_KEYS = ["phone_numbers", "active_phone_numbers", "drafted_phone_numb
102
110
  const LINKEDIN_KEYS = ["linkedin_accounts", "active_linkedin_accounts", "drafted_linkedin_accounts"];
103
111
  const phoneFits = (p: PhoneOption, type: "ai" | "dialer") => !p.inbound_type || p.inbound_type === type;
104
112
  const linkedinLabel = (a: LinkedinOption) => a.name || a.display_name || a.public_identifier || a.email || `Account ${a.id}`;
105
- interface ListOption {
106
- id: number;
107
- title: string;
108
- contact_count?: number | null;
109
- }
113
+ /** The mailboxes API has no paging: a full read is announced under the sender dropdown. */
114
+ const MAILBOX_LIMIT = 200;
110
115
  type Pending = { kind: "create" } | { kind: "update" | "delete"; step: DraftStep };
111
116
 
112
117
  function describe(p: Pending, live: boolean): { tier: ConfirmTier; title: string; description: string; label: string } {
@@ -134,16 +139,17 @@ function describe(p: Pending, live: boolean): { tier: ConfirmTier; title: string
134
139
  };
135
140
  }
136
141
 
137
- export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved, className }: SequenceBuilderProps) {
142
+ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved, initialName, initialSteps, className }: SequenceBuilderProps) {
138
143
  const brand = useBrand();
139
144
  const editing = Boolean(sequenceId);
140
- const [name, setName] = useState("");
145
+ const [name, setName] = useState(initialName ?? "");
141
146
  const [mailboxId, setMailboxId] = useState("");
142
147
  const [aiPhoneId, setAiPhoneId] = useState("");
143
148
  const [dialerPhoneId, setDialerPhoneId] = useState("");
144
149
  const [linkedinId, setLinkedinId] = useState("");
145
150
  const [listId, setListId] = useState(defaultListId ? String(defaultListId) : "");
146
- const [steps, setSteps] = useState<DraftStep[]>([]);
151
+ const [steps, setSteps] = useState<DraftStep[]>(() => (sequenceId ? [] : (initialSteps ?? [])));
152
+ const [pickingTemplate, setPickingTemplate] = useState(false);
147
153
  const [loadedFor, setLoadedFor] = useState<string | null>(null);
148
154
  const [pending, setPending] = useState<Pending | null>(null);
149
155
  const [saved, setSaved] = useState<string | null>(null);
@@ -157,8 +163,7 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
157
163
  // Senders already attached in edit mode by an earlier (failed) append: never attach them twice.
158
164
  const attached = useRef<Set<string>>(new Set());
159
165
 
160
- const mailboxes = useG8Query((signal) => g8Get("/mailboxes", { limit: 200 }, { signal }), [], { enabled: !editing });
161
- const lists = useG8Query((signal) => g8Get("/lists", { limit: 100 }, { signal }), [], { enabled: !editing });
166
+ const mailboxes = useG8Query((signal) => g8Get("/mailboxes", { limit: MAILBOX_LIMIT }, { signal }), [], { enabled: !editing });
162
167
  const existing = useG8Query(
163
168
  async (signal) => {
164
169
  const [seq, list] = await Promise.all([
@@ -323,15 +328,17 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
323
328
  );
324
329
  })}
325
330
  </select>
326
- <select aria-label="Audience list" className={select} value={listId} onChange={(e) => setListId(e.target.value)}>
327
- <option value="">Audience list (required)</option>
328
- {listOf<ListOption>(lists.data).map((l) => (
329
- <option key={l.id} value={l.id}>
330
- {l.title}
331
- {l.contact_count != null ? ` (${l.contact_count})` : ""}
332
- </option>
333
- ))}
334
- </select>
331
+ <ListSelect
332
+ label="Audience list"
333
+ emptyOption="Audience list (required)"
334
+ optionLabel={(l) => `${l.title}${l.contact_count != null ? ` (${l.contact_count})` : ""}`}
335
+ pageSize={100}
336
+ value={listId}
337
+ valueLabel={defaultListId ? `List ${defaultListId}` : undefined}
338
+ selectClassName={select}
339
+ onChange={(l) => setListId(l ? String(l.id) : "")}
340
+ />
341
+ <FirstNotice className="sm:col-span-3" shown={listOf<MailboxOption>(mailboxes.data).length} limit={MAILBOX_LIMIT} noun="mailboxes" />
335
342
  </div>
336
343
  ) : null}
337
344
  {need.aiPhone || need.dialerPhone || need.linkedin ? (
@@ -373,12 +380,11 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
373
380
  {String(objectOf(linkedin.data).message ?? "No LinkedIn account is connected.")} Connect one in the {brand.linkLabel} app.
374
381
  </p>
375
382
  ) : null}
376
- {mailboxes.error || lists.error || phones.error || linkedin.error ? (
383
+ {mailboxes.error || phones.error || linkedin.error ? (
377
384
  <ApiError
378
- error={mailboxes.error ?? lists.error ?? phones.error ?? linkedin.error}
385
+ error={mailboxes.error ?? phones.error ?? linkedin.error}
379
386
  onRetry={() => {
380
387
  void mailboxes.refetch();
381
- void lists.refetch();
382
388
  if (need.aiPhone || need.dialerPhone) void phones.refetch();
383
389
  if (need.linkedin) void linkedin.refetch();
384
390
  }}
@@ -393,6 +399,7 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
393
399
  index={i + 1}
394
400
  onChange={(next) => setSteps((all) => all.map((x) => (x.key === s.key ? next : x)))}
395
401
  onRemove={() => (s.id ? setPending({ kind: "delete", step: s }) : setSteps((all) => all.filter((x) => x.key !== s.key)))}
402
+ renderAssist={(step, change) => <StepAiAssist step={step} onChange={change} sequenceId={sequenceId} stepOrder={i + 1} />}
396
403
  />
397
404
  <div className="flex gap-2 text-xs">
398
405
  {!s.id ? (
@@ -422,7 +429,9 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
422
429
  Add {KIND_LABEL[k].split(" ").map((w) => (/^[A-Z]{2,}$/.test(w) ? w : w.toLowerCase())).join(" ")}
423
430
  </Button>
424
431
  ))}
432
+ <Button size="sm" variant="ghost" onClick={() => setPickingTemplate((p) => !p)}>{pickingTemplate ? "Hide step templates" : "From a step template"}</Button>
425
433
  </div>
434
+ {pickingTemplate ? <StepTemplatePicker onPick={(step) => { setSteps((s) => [...s, step]); setPickingTemplate(false); }} /> : null}
426
435
 
427
436
  {editing ? (
428
437
  <Button
@@ -449,7 +458,7 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
449
458
  {run.error ? <ApiError error={run.error} onRetry={run.reset} /> : null}
450
459
  {append.error ? <ApiError error={append.error} onRetry={append.reset} /> : null}
451
460
  {saved ? (
452
- <p role="status" className="text-sm text-green-700">
461
+ <p role="status" className="text-sm text-positive-foreground">
453
462
  {saved}
454
463
  </p>
455
464
  ) : null}
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SequenceCampaignLink: which campaign a sequence belongs to. Shows the linked campaign with an "Open campaign"
5
+ * button, lets you unlink it, or, when there is none, pick a campaign and link it. Use it in
6
+ * `SequencesScreen.renderDetailActions` (the row it passes is the full sequence, so it carries the campaign id):
7
+ * renderDetailActions={(s) => <SequenceCampaignLink sequence={s} onOpenCampaign={(id) => router.push(`/campaigns?id=${id}`)} />}
8
+ *
9
+ * SDK operations, in order:
10
+ * 1. getCampaign GET /campaigns/{id} (read; the linked campaign's name)
11
+ * listCampaignsForPicker GET /campaigns/picker (read; the choices, only while the sequence has no campaign)
12
+ * 2. linkSequenceCampaign PUT /sequences/{id}/campaign (write)
13
+ * 3. unlinkSequenceCampaign DELETE /sequences/{id}/campaign (DESTRUCTIVE tier -> ConfirmSpendDialog; the campaign is kept)
14
+ * Linking never replaces silently: while a campaign is linked, the picker is hidden, so you unlink first.
15
+ */
16
+
17
+ import { useState } from "react";
18
+ import { Button, Skeleton } from "@graph8/sdk/ui";
19
+ import { ExternalLink, Link2, Unlink } from "lucide-react";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
22
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
24
+ import { g8Delete, g8Get, g8Put } from "@/g8/lib/g8-client";
25
+ import { listOf, objectOf } from "../lib/sequencer-utils";
26
+
27
+ export interface SequenceCampaignLinkProps {
28
+ /** The full sequence (from `renderDetailActions`): its id and `campaign_builder_campaign_id`. */
29
+ sequence: { id: string; name?: string | null; campaign_builder_campaign_id?: string | null };
30
+ /** Route to the campaign in your app. Without it the campaign's name is shown without a button. */
31
+ onOpenCampaign?: (campaignId: string) => void;
32
+ }
33
+
34
+ interface Choice { id: string; name?: string | null }
35
+
36
+ export function SequenceCampaignLink({ sequence, onOpenCampaign }: SequenceCampaignLinkProps) {
37
+ const linked = sequence.campaign_builder_campaign_id ? String(sequence.campaign_builder_campaign_id) : "";
38
+ const [open, setOpen] = useState(false);
39
+ const [choice, setChoice] = useState("");
40
+ const [confirm, setConfirm] = useState(false);
41
+ const campaign = useG8Query((signal) => g8Get(`/campaigns/${encodeURIComponent(linked)}`, undefined, { signal }), [linked], { enabled: Boolean(linked), key: ["campaigns", "sequence-link", linked] });
42
+ const choices = useG8Query((signal) => g8Get("/campaigns/picker", undefined, { signal }), [], { enabled: open && !linked, key: ["sequences", "campaign-choices"] });
43
+ const refresh = [["sequences", "detail", sequence.id], ["sequences", "list"]] as const;
44
+ const link = useG8Mutation((id: string) => g8Put(`/sequences/${encodeURIComponent(sequence.id)}/campaign`, { campaign_id: id }), { invalidates: refresh, onSuccess: () => { setOpen(false); setChoice(""); } });
45
+ const unlink = useG8Mutation(async () => {
46
+ try { await g8Delete(`/sequences/${encodeURIComponent(sequence.id)}/campaign`); } catch (err) { if ((err as { status?: number }).status !== 404) throw err; }
47
+ }, { invalidates: refresh, onSuccess: () => setConfirm(false), onError: () => setConfirm(false) });
48
+
49
+ const name = (objectOf(campaign.data).name as string | undefined) || `Campaign ${linked}`;
50
+ return (
51
+ <>
52
+ {linked ? (
53
+ <>
54
+ {campaign.loading && !campaign.data ? <Skeleton className="h-8 w-28" aria-label="Loading campaign" /> : onOpenCampaign ? (
55
+ <Button size="sm" variant="outline" onClick={() => onOpenCampaign(linked)} aria-label={`Open campaign ${name}`}><ExternalLink className="mr-1 h-4 w-4" aria-hidden />Open campaign</Button>
56
+ ) : <span className="text-sm text-muted-foreground">Campaign: {name}</span>}
57
+ <Button size="sm" variant="ghost" onClick={() => setConfirm(true)}><Unlink className="mr-1 h-4 w-4" aria-hidden />Unlink campaign</Button>
58
+ </>
59
+ ) : open ? (
60
+ <span className="flex items-center gap-2">
61
+ {choices.error ? <ApiError error={choices.error} title="Could not load campaigns" onRetry={() => void choices.refetch()} /> : (
62
+ <select aria-label="Campaign to link" className="h-8 rounded-md border bg-background px-2 text-sm" value={choice} onChange={(e) => setChoice(e.target.value)}>
63
+ <option value="">{choices.loading && !choices.data ? "Loading campaigns" : listOf<Choice>(choices.data).length ? "Choose a campaign" : "No campaigns yet"}</option>
64
+ {listOf<Choice>(choices.data).map((c) => <option key={c.id} value={c.id}>{c.name ?? c.id}</option>)}
65
+ </select>
66
+ )}
67
+ <Button size="sm" disabled={!choice} loading={link.loading} onClick={() => void link.mutate(choice)}>Link</Button>
68
+ <Button size="sm" variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
69
+ </span>
70
+ ) : (
71
+ <Button size="sm" variant="outline" onClick={() => setOpen(true)}><Link2 className="mr-1 h-4 w-4" aria-hidden />Link to campaign</Button>
72
+ )}
73
+ {link.error ? <ApiError error={link.error} title="Could not link the campaign" onRetry={link.reset} /> : null}
74
+ {unlink.error ? <ApiError error={unlink.error} title="Could not unlink the campaign" onRetry={unlink.reset} /> : null}
75
+ <ConfirmSpendDialog open={confirm} tier="destructive" title={`Unlink "${sequence.name ?? "this sequence"}" from ${name}?`} description="The campaign and the sequence are both kept. Only the tie between them is removed." confirmLabel="Unlink" affected={1} confirming={unlink.loading} onCancel={() => setConfirm(false)} onConfirm={() => void unlink.mutate(undefined)} />
76
+ </>
77
+ );
78
+ }
@@ -17,6 +17,8 @@
17
17
  * 5. archiveSequence POST /sequences/{id}/archive (destructive: confirmed)
18
18
  * Enroll contacts: EnrollSheet (ListPicker + ContactTable, or the whole list) then EnrollDialog (external: confirmed).
19
19
  * Steps, Contacts and Results tabs compose SequenceStepsPanel, SequenceEnrollments and SequenceStats.
20
+ * Manage (SequenceManageActions): edit and rename (external, confirmed), sync the audience (external, confirmed), save as
21
+ * template, delete (destructive, confirmed). The Senders tab (SequenceSendersPanel) lists the senders and removes one (destructive, confirmed).
20
22
  */
21
23
 
22
24
  import { useState } from "react";
@@ -39,6 +41,8 @@ import { fieldItems, launchCheck, listOf, objectOf, statusGroup } from "../lib/s
39
41
  import type { SequenceRow } from "../lib/sequencer-utils";
40
42
  import { EnrollDialog } from "./enroll-dialog";
41
43
  import { SequenceEnrollments } from "./sequence-enrollments";
44
+ import { SequenceManageActions } from "./sequence-manage-actions";
45
+ import { SequenceSendersPanel } from "./sequence-senders-panel";
42
46
  import { SequenceStats } from "./sequence-stats";
43
47
  import { SequenceStepsPanel } from "./sequence-steps-panel";
44
48
  import { pluralWord } from "@/g8/lib/plural";
@@ -143,6 +147,8 @@ export interface SequenceDetailProps {
143
147
  refreshKey?: number;
144
148
  /** Open another sequence (the duplicate). */
145
149
  onOpenSequence?: (sequenceId: string) => void;
150
+ /** Open an enrolled person in your contacts screen. */
151
+ onOpenContact?: (contactId: number) => void;
146
152
  renderActions?: (sequence: SequenceRow) => ReactNode;
147
153
  extraTabs?: { id: string; label: string; render: (sequence: SequenceRow) => ReactNode }[];
148
154
  className?: string;
@@ -201,6 +207,7 @@ export function SequenceDetail(props: SequenceDetailProps) {
201
207
  <Button size="sm" variant="outline" onClick={() => setEnrolling(true)}><UserPlus className="mr-1 h-4 w-4" aria-hidden /> Enroll contacts</Button>
202
208
  <Button size="sm" variant="ghost" aria-label="Duplicate sequence" loading={duplicate.loading} onClick={() => void duplicate.mutate(undefined)}><Copy className="h-4 w-4" aria-hidden /></Button>
203
209
  {group !== "ended" ? <Button size="sm" variant="ghost" onClick={() => void prepare.mutate("archive")}>Archive sequence</Button> : null}
210
+ <SequenceManageActions sequence={row} onChanged={changed} onDeleted={() => onClose?.()} />
204
211
  {props.renderActions?.(row)}
205
212
  {enrolling ? <EnrollSheet sequence={row} onClose={() => setEnrolling(false)} onEnrolled={() => { setEnrolling(false); setStatsKey((k) => k + 1); changed(); }} /> : null}
206
213
  {p ? (
@@ -211,7 +218,7 @@ export function SequenceDetail(props: SequenceDetailProps) {
211
218
  description={
212
219
  <>
213
220
  <p>{COPY[p.action].description}</p>
214
- {p.warnings.length > 0 ? <ul className="mt-2 list-disc pl-5 text-amber-800">{p.warnings.map((w) => <li key={w}>{w}</li>)}</ul> : null}
221
+ {p.warnings.length > 0 ? <ul className="mt-2 list-disc pl-5 text-warning-foreground">{p.warnings.map((w) => <li key={w}>{w}</li>)}</ul> : null}
215
222
  </>
216
223
  }
217
224
  recipients={p.action === "archive" ? undefined : (row.contact_count ?? undefined)}
@@ -250,7 +257,8 @@ export function SequenceDetail(props: SequenceDetailProps) {
250
257
  ),
251
258
  },
252
259
  { id: "steps", label: "Steps", count: row.step_count ?? undefined, content: <SequenceStepsPanel sequenceId={id} stepId={props.stepId} onStepChange={props.onStepChange} onChanged={changed} /> },
253
- { id: "contacts", label: "Contacts", count: row.contact_count ?? undefined, content: <SequenceEnrollments key={statsKey} sequenceId={id} contactId={props.contactId} onContactChange={props.onContactChange} /> },
260
+ { id: "contacts", label: "Contacts", count: row.contact_count ?? undefined, content: <SequenceEnrollments key={statsKey} sequenceId={id} contactId={props.contactId} onContactChange={props.onContactChange} onOpenContact={props.onOpenContact} /> },
261
+ { id: "senders", label: "Senders", content: <SequenceSendersPanel sequenceId={id} onChanged={changed} /> },
254
262
  { id: "results", label: "Results", content: <SequenceStats key={`${id}:${statsKey}`} sequenceId={id} refreshSeconds={group === "live" ? 60 : undefined} /> },
255
263
  ...(props.extraTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(row) })),
256
264
  ]}
@@ -0,0 +1,111 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SequenceEditForm: rename a sequence and change its own settings (description, stop on reply, same email thread, pick
5
+ * up new contacts).
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. getSequence GET /sequences/{id} (read) the record the form is seeded from
9
+ * 2. updateSequence PATCH /sequences/{id} (EXTERNAL: tier `external`, because changing a live sequence changes what
10
+ * real people receive) -> ConfirmSpendDialog first, naming each change and the contacts in flight
11
+ *
12
+ * The form never saves over a record it has not loaded: nothing renders until the sequence has loaded, Save stays
13
+ * disabled until something differs from what was loaded, and the request carries ONLY the changed fields (a partial
14
+ * PATCH, so a field this form does not show is never reset). A 409 (the sequence is mid-transition: launching, pausing)
15
+ * is shown as the API words it. The audience list, mailboxes and steps are not edited here: the API cannot change the
16
+ * list after creation, senders are SequenceSendersPanel, steps are SequenceStepsPanel.
17
+ */
18
+ import { useState } from "react";
19
+ import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
22
+ import { g8Get, g8Patch } from "@/g8/lib/g8-client";
23
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
24
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
25
+ import { changeSummary, editableSequence, sequenceChanges } from "../lib/sequencer-manage";
26
+ import type { EditableSequence } from "../lib/sequencer-manage";
27
+ import { objectOf, statusGroup } from "../lib/sequencer-utils";
28
+
29
+ export interface SequenceEditFormProps {
30
+ sequenceId: string;
31
+ /** Called after the change was saved. */
32
+ onSaved?: () => void;
33
+ onCancel?: () => void;
34
+ className?: string;
35
+ }
36
+
37
+ function Fields({ loaded, contacts, live, sequenceId, onSaved, onCancel }: { loaded: EditableSequence; contacts: number; live: boolean; sequenceId: string; onSaved?: () => void; onCancel?: () => void }) {
38
+ const [draft, setDraft] = useState<EditableSequence>(loaded);
39
+ const [confirming, setConfirming] = useState(false);
40
+ const changes = sequenceChanges(loaded, draft);
41
+ const dirty = Object.keys(changes).length > 0;
42
+ const save = useG8Mutation(async () => {
43
+ await g8Patch(`/sequences/${encodeURIComponent(sequenceId)}`, changes);
44
+ }, { invalidates: ["sequences"] });
45
+ const set = <K extends keyof EditableSequence>(key: K, value: EditableSequence[K]) => setDraft((d) => ({ ...d, [key]: value }));
46
+ const confirm = async () => {
47
+ try {
48
+ await save.mutateAsync(undefined);
49
+ setConfirming(false);
50
+ onSaved?.();
51
+ } catch {
52
+ setConfirming(false);
53
+ }
54
+ };
55
+ const check = (key: "finish_on_reply" | "send_in_same_thread" | "wait_for_new_contacts", label: string) => (
56
+ <label className="flex items-center gap-2 text-sm">
57
+ <input type="checkbox" checked={draft[key]} onChange={(e) => set(key, e.target.checked)} />
58
+ {label}
59
+ </label>
60
+ );
61
+ return (
62
+ <form className="space-y-3" onSubmit={(e) => { e.preventDefault(); if (dirty && draft.name.trim()) setConfirming(true); }}>
63
+ <label className="block space-y-1 text-sm"><span className="font-medium">Name</span><Input aria-label="Sequence name" value={draft.name} onChange={(e) => set("name", e.target.value)} /></label>
64
+ <label className="block space-y-1 text-sm"><span className="font-medium">Description</span><Textarea aria-label="Description" rows={3} value={draft.description} onChange={(e) => set("description", e.target.value)} /></label>
65
+ <div className="space-y-2">
66
+ {check("finish_on_reply", "Stop sending to a contact once they reply")}
67
+ {check("send_in_same_thread", "Send follow-up emails in the same thread")}
68
+ {check("wait_for_new_contacts", "Keep picking up contacts added to the audience list")}
69
+ </div>
70
+ {live ? <p role="note" className="text-xs text-warning-foreground">This sequence is live. Changes apply to contacts already in it.</p> : null}
71
+ {save.error ? <ApiError error={save.error} title="Could not save the sequence" /> : null}
72
+ <div className="flex justify-end gap-2">
73
+ {onCancel ? <Button type="button" variant="outline" onClick={onCancel}>Cancel</Button> : null}
74
+ <Button type="submit" disabled={!dirty || !draft.name.trim() || save.loading}>Save changes</Button>
75
+ </div>
76
+ <ConfirmSpendDialog
77
+ open={confirming}
78
+ tier="external"
79
+ title={`Save changes to "${loaded.name}"?`}
80
+ description={<ul className="list-disc pl-5">{changeSummary(changes).map((c) => <li key={c}>{c}</li>)}</ul>}
81
+ recipients={contacts > 0 ? contacts : undefined}
82
+ confirmLabel="Save changes"
83
+ confirming={save.loading}
84
+ onConfirm={() => void confirm()}
85
+ onCancel={() => setConfirming(false)}
86
+ />
87
+ </form>
88
+ );
89
+ }
90
+
91
+ export function SequenceEditForm({ sequenceId, onSaved, onCancel, className }: SequenceEditFormProps) {
92
+ const [version, setVersion] = useState(0);
93
+ // No refetch on window focus: it would reseed the form and wipe what the person is typing.
94
+ const seq = useG8Query((signal) => g8Get(`/sequences/${encodeURIComponent(sequenceId)}`, undefined, { signal }), [sequenceId], { key: ["sequences", "edit", sequenceId], keepPrevious: false, refetchOnFocus: false });
95
+ if (seq.error) return <ApiError error={seq.error} title="Could not load the sequence" onRetry={() => void seq.refetch()} className={className} />;
96
+ if (!seq.data || seq.isPlaceholderData) return <Skeleton aria-busy="true" className={`h-48 w-full ${className ?? ""}`} />;
97
+ const record = objectOf(seq.data);
98
+ return (
99
+ <div className={className}>
100
+ <Fields
101
+ key={`${sequenceId}:${version}`}
102
+ loaded={editableSequence(record)}
103
+ contacts={Number(record.contact_count ?? 0) || 0}
104
+ live={statusGroup(typeof record.status === "string" ? record.status : null) === "live"}
105
+ sequenceId={sequenceId}
106
+ onSaved={() => { void seq.refetch().then(() => setVersion((v) => v + 1)); onSaved?.(); }}
107
+ onCancel={onCancel}
108
+ />
109
+ </div>
110
+ );
111
+ }
@@ -24,6 +24,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
24
24
  import { useG8Query } from "@/g8/hooks/use-g8-query";
25
25
  import { g8Get, g8Post } from "@/g8/lib/g8-client";
26
26
  import { ContactDetail } from "@/g8/crm/components/contact-detail";
27
+ import { WithdrawContactButton } from "./enrollment-withdraw";
27
28
  import { fieldItems, listOf } from "../lib/sequencer-utils";
28
29
 
29
30
  export interface SequenceEnrollmentsProps {
@@ -31,6 +32,8 @@ export interface SequenceEnrollmentsProps {
31
32
  /** The open enrollment, by contact id. Controlled so a screen can deep-link it. */
32
33
  contactId?: string;
33
34
  onContactChange?: (contactId: string | null) => void;
35
+ /** Open the enrolled person in your contacts screen (adds an "Open in contacts" button). */
36
+ onOpenContact?: (contactId: number) => void;
34
37
  pageSize?: number;
35
38
  className?: string;
36
39
  }
@@ -41,7 +44,7 @@ const STATES = ["", "waiting", "paused", "completed", "replied", "bounced", "ski
41
44
  const TONE: Record<string, Tone> = { waiting: "info", paused: "warning", completed: "positive", replied: "positive", bounced: "negative", skipped: "neutral" };
42
45
  const nameOf = (e: Enrollment) => e.name || e.email || (e.contact_id != null ? `Contact ${e.contact_id}` : "Contact");
43
46
 
44
- export function SequenceEnrollments({ sequenceId, contactId, onContactChange, pageSize = 25, className }: SequenceEnrollmentsProps) {
47
+ export function SequenceEnrollments({ sequenceId, contactId, onContactChange, onOpenContact, pageSize = 25, className }: SequenceEnrollmentsProps) {
45
48
  const [state, setState] = useState("");
46
49
  const [page, setPage] = useState(1);
47
50
  const [resume, setResume] = useState<Enrollment | null>(null);
@@ -89,7 +92,7 @@ export function SequenceEnrollments({ sequenceId, contactId, onContactChange, pa
89
92
  <Button size="sm" variant="outline" disabled={rows.length < pageSize} onClick={() => setPage((p) => p + 1)}>Next</Button>
90
93
  </div>
91
94
  {open ? (
92
- <SectionCard title={`${nameOf(open)} in this sequence`} actions={<>{actions(open)}<Button size="sm" variant="ghost" onClick={() => onContactChange?.(null)}>Close</Button></>}>
95
+ <SectionCard title={`${nameOf(open)} in this sequence`} actions={<>{actions(open)}{onOpenContact && open.contact_id != null ? <Button size="sm" variant="outline" onClick={() => onOpenContact(open.contact_id as number)}>Open in contacts</Button> : null}{open.contact_id != null ? <WithdrawContactButton sequenceId={sequenceId} contactId={open.contact_id} contactName={nameOf(open)} onWithdrawn={() => { onContactChange?.(null); void list.refetch(); }} /> : null}<Button size="sm" variant="ghost" onClick={() => onContactChange?.(null)}>Close</Button></>}>
93
96
  <KeyValueList columns={2} items={fieldItems(open as Record<string, unknown>)} />
94
97
  {open.contact_id != null ? (
95
98
  <SectionCard title="Contact" description="The full contact record.">
@@ -204,7 +204,7 @@ export function SequenceList({ status, pageSize = 25, onOpen, onCreate, onChange
204
204
  <>
205
205
  <p>{COPY[pending.action].description}</p>
206
206
  {pending.warnings.length > 0 ? (
207
- <ul className="mt-2 list-disc pl-5 text-amber-800">
207
+ <ul className="mt-2 list-disc pl-5 text-warning-foreground">
208
208
  {pending.warnings.map((w) => (
209
209
  <li key={w}>{w}</li>
210
210
  ))}
@@ -0,0 +1,119 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SequenceManageActions: the buttons that manage a sequence itself, for a sequence detail's action row: Edit (rename and
5
+ * settings), Sync audience, Save as template, Delete.
6
+ *
7
+ * SDK operations, in order:
8
+ * Edit SequenceEditForm in a sheet: getSequence (read) then updateSequence PATCH /sequences/{id} (EXTERNAL, confirmed there)
9
+ * Sync audience POST /sequences/{id}/sync (sequences.syncSequence) EXTERNAL -> ConfirmSpendDialog first.
10
+ * Re-pulls the audience list so contacts added to it since launch are ENROLLED (real recipients).
11
+ * Save as template GET /sequences/{id}/steps (sequenceLifecycle.listSequenceSteps) read
12
+ * POST /sequencer/templates/sequences (sequences.createSequenceTemplate) write; the steps are copied, not linked
13
+ * Delete DELETE /sequences/{id} (sequenceLifecycle.deleteSequence) DESTRUCTIVE -> ConfirmSpendDialog first.
14
+ * The API's DELETE is a SOFT delete, the same as archive: the sequence stops, leaves every list, and cannot be
15
+ * restored through the API. The Archive button in SequenceDetail stays for the same effect.
16
+ */
17
+ import { useState } from "react";
18
+ import { BookmarkPlus, Pencil, RefreshCw, Trash2 } from "lucide-react";
19
+ import { Button, Input } from "@graph8/sdk/ui";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
22
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
23
+ import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
24
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
25
+ import { templateBodyFromSteps } from "../lib/sequencer-manage";
26
+ import { listOf, statusGroup } from "../lib/sequencer-utils";
27
+ import type { ApiStep, SequenceRow } from "../lib/sequencer-utils";
28
+ import { SequenceEditForm } from "./sequence-edit-form";
29
+
30
+ export interface SequenceManageActionsProps {
31
+ sequence: SequenceRow;
32
+ /** Called after any change so the list and detail can refresh. */
33
+ onChanged?: () => void;
34
+ /** Called after the sequence was deleted (close its detail here). */
35
+ onDeleted?: () => void;
36
+ className?: string;
37
+ }
38
+
39
+ type Dialog = "sync" | "delete" | "template" | null;
40
+
41
+ export function SequenceManageActions({ sequence, onChanged, onDeleted, className }: SequenceManageActionsProps) {
42
+ const [dialog, setDialog] = useState<Dialog>(null);
43
+ const [editing, setEditing] = useState(false);
44
+ const [templateName, setTemplateName] = useState("");
45
+ const [notice, setNotice] = useState<string | null>(null);
46
+ const name = sequence.name ?? `Sequence ${sequence.id}`;
47
+ const group = statusGroup(sequence.status);
48
+ const id = encodeURIComponent(sequence.id);
49
+
50
+ const sync = useG8Mutation(async () => {
51
+ await g8Post(`/sequences/${id}/sync`);
52
+ setDialog(null);
53
+ setNotice("The audience list was synced.");
54
+ onChanged?.();
55
+ }, { invalidates: ["sequences"], onError: () => setDialog(null) });
56
+ const remove = useG8Mutation(async () => {
57
+ await g8Delete(`/sequences/${id}`);
58
+ setDialog(null);
59
+ onChanged?.();
60
+ onDeleted?.();
61
+ }, { invalidates: ["sequences"], onError: () => setDialog(null) });
62
+ const saveTemplate = useG8Mutation(async () => {
63
+ const steps = listOf<ApiStep>(await g8Get(`/sequences/${id}/steps`));
64
+ if (steps.length === 0) throw new Error("This sequence has no steps to save.");
65
+ await g8Post("/sequencer/templates/sequences", templateBodyFromSteps(templateName, "", steps));
66
+ setDialog(null);
67
+ setNotice(`Saved "${templateName.trim()}" as a template.`);
68
+ }, { onError: () => undefined });
69
+
70
+ return (
71
+ <>
72
+ <span className={`contents ${className ?? ""}`}>
73
+ <Button size="sm" variant="outline" onClick={() => setEditing(true)}><Pencil className="mr-1 h-4 w-4" aria-hidden />Edit</Button>
74
+ {group === "live" || group === "paused" ? <Button size="sm" variant="outline" onClick={() => setDialog("sync")}><RefreshCw className="mr-1 h-4 w-4" aria-hidden />Sync audience</Button> : null}
75
+ <Button size="sm" variant="ghost" onClick={() => { setTemplateName(`${name} template`); setDialog("template"); }}><BookmarkPlus className="mr-1 h-4 w-4" aria-hidden />Save as template</Button>
76
+ <Button size="sm" variant="ghost" onClick={() => setDialog("delete")}><Trash2 className="mr-1 h-4 w-4" aria-hidden />Delete sequence</Button>
77
+ </span>
78
+ {notice ? <p role="status" className="basis-full text-xs text-muted-foreground">{notice}</p> : null}
79
+ {[sync.error, remove.error].map((e, i) => (e ? <ApiError key={i} error={e} title="Could not update the sequence" /> : null))}
80
+
81
+ <FormSheet open={editing} onOpenChange={setEditing} title={`Edit ${name}`} description="Rename it or change how it behaves. Only what you change is saved.">
82
+ {editing ? <SequenceEditForm sequenceId={sequence.id} onCancel={() => setEditing(false)} onSaved={() => { setEditing(false); onChanged?.(); }} /> : null}
83
+ </FormSheet>
84
+
85
+ <FormSheet
86
+ open={dialog === "template"}
87
+ onOpenChange={(o) => !o && setDialog(null)}
88
+ title="Save as template"
89
+ description="Copies the steps so you can start new sequences from them. The template does not change when this sequence does."
90
+ footer={<><Button variant="outline" onClick={() => setDialog(null)}>Cancel</Button><Button loading={saveTemplate.loading} disabled={!templateName.trim()} onClick={() => void saveTemplate.mutate(undefined)}>Save template</Button></>}
91
+ >
92
+ <Input aria-label="Template name" value={templateName} onChange={(e) => setTemplateName(e.target.value)} />
93
+ {saveTemplate.error ? <ApiError error={saveTemplate.error} title="Could not save the template" /> : null}
94
+ </FormSheet>
95
+
96
+ <ConfirmSpendDialog
97
+ open={dialog === "sync"}
98
+ tier="external"
99
+ title={`Sync the audience of "${name}"?`}
100
+ description="Contacts added to the audience list since the last sync are enrolled now and start receiving the steps."
101
+ confirmLabel="Sync audience"
102
+ confirming={sync.loading}
103
+ onConfirm={() => void sync.mutate(undefined)}
104
+ onCancel={() => setDialog(null)}
105
+ />
106
+ <ConfirmSpendDialog
107
+ open={dialog === "delete"}
108
+ tier="destructive"
109
+ title={`Delete "${name}"?`}
110
+ description="The sequence stops sending and disappears from your lists. The API cannot restore a deleted sequence. Contacts already messaged keep what they received."
111
+ confirmLabel="Delete sequence"
112
+ affected={1}
113
+ confirming={remove.loading}
114
+ onConfirm={() => void remove.mutate(undefined)}
115
+ onCancel={() => setDialog(null)}
116
+ />
117
+ </>
118
+ );
119
+ }
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SequenceSendersPanel: the senders a sequence sends from (mailboxes, phone numbers, LinkedIn accounts) and a way to
5
+ * take one off.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listSequenceChannels GET /sequences/{id}/channels (read)
9
+ * 2. removeSequenceChannel DELETE /sequences/{id}/channels/{sequence_channel_id} (DESTRUCTIVE) -> ConfirmSpendDialog first
10
+ * Removing the only sender of a step type means those steps can no longer send and the sequence cannot launch (or keeps
11
+ * a live step stuck); the dialog says which step types lose their sender. Adding senders to an existing sequence is
12
+ * done where its steps are added (SequenceBuilder in edit mode), which asks for the sender each new step type needs.
13
+ */
14
+ import { useState } from "react";
15
+ import { Trash2 } from "lucide-react";
16
+ import { Button, Skeleton } from "@graph8/sdk/ui";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
19
+ import { EmptyState } from "@/g8/components/empty-state";
20
+ import { StatusPill } from "@/g8/components/screen/status-pill";
21
+ import { g8Delete, g8Get } from "@/g8/lib/g8-client";
22
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
24
+ import { listOf } from "../lib/sequencer-utils";
25
+
26
+ export interface SequenceSendersPanelProps {
27
+ sequenceId: string;
28
+ /** Called after a sender was removed so a detail can recheck whether the sequence can launch. */
29
+ onChanged?: () => void;
30
+ className?: string;
31
+ }
32
+
33
+ interface Channel { id?: string | number; channel_id?: number; channel_type?: string | null; channel_value?: string | null }
34
+
35
+ const LABEL: Record<string, string> = { GMAIL: "Gmail mailbox", INBOXKIT: "Managed mailbox", SMTP: "SMTP mailbox", PHONE: "Phone number", LINKEDIN: "LinkedIn account", LINKEDIN_NETRION: "LinkedIn account", SMS: "SMS number", WHATSAPP: "WhatsApp number" };
36
+ const STEPS_LOST: Record<string, string> = { GMAIL: "email", INBOXKIT: "email", SMTP: "email", PHONE: "call", LINKEDIN: "LinkedIn", LINKEDIN_NETRION: "LinkedIn", SMS: "SMS", WHATSAPP: "WhatsApp" };
37
+ const typeOf = (c: Channel) => (c.channel_type ?? "").toUpperCase();
38
+
39
+ export function SequenceSendersPanel({ sequenceId, onChanged, className }: SequenceSendersPanelProps) {
40
+ const [removing, setRemoving] = useState<Channel | null>(null);
41
+ const list = useG8Query((signal) => g8Get(`/sequences/${encodeURIComponent(sequenceId)}/channels`, undefined, { signal }), [sequenceId], { key: ["sequences", "channels", sequenceId] });
42
+ const rows = listOf<Channel>(list.data);
43
+ const remove = useG8Mutation(async (c: Channel) => {
44
+ await g8Delete(`/sequences/${encodeURIComponent(sequenceId)}/channels/${encodeURIComponent(String(c.id))}`);
45
+ setRemoving(null);
46
+ await list.refetch();
47
+ onChanged?.();
48
+ }, { invalidates: ["sequences"], onError: () => setRemoving(null) });
49
+
50
+ if (list.error) return <ApiError error={list.error} title="Could not load the senders" onRetry={() => void list.refetch()} className={className} />;
51
+ if (list.loading && !list.data) return <Skeleton aria-busy="true" className={`h-24 w-full ${className ?? ""}`} />;
52
+ const others = removing ? rows.filter((c) => c.id !== removing.id && STEPS_LOST[typeOf(c)] === STEPS_LOST[typeOf(removing)]).length : 0;
53
+
54
+ return (
55
+ <div className={`space-y-3 ${className ?? ""}`}>
56
+ <h3 className="font-medium">Senders</h3>
57
+ {rows.length === 0 ? (
58
+ <EmptyState compact title="No sender yet" description="Steps need a sender to go out: a mailbox for emails, a number for calls, an account for LinkedIn. Add steps with their sender in the Steps tab." />
59
+ ) : (
60
+ <ul aria-label="Senders" className="divide-y rounded-md border border-border text-sm">
61
+ {rows.map((c) => (
62
+ <li key={String(c.id ?? `${c.channel_type}:${c.channel_id}`)} className="flex items-center gap-3 p-2">
63
+ <StatusPill tone="neutral">{LABEL[typeOf(c)] ?? (c.channel_type || "Sender")}</StatusPill>
64
+ <span className="min-w-0 flex-1 truncate">{c.channel_value || `#${c.channel_id ?? ""}`}</span>
65
+ <Button size="sm" variant="ghost" aria-label={`Remove sender ${c.channel_value ?? c.channel_id ?? ""}`} disabled={c.id == null} onClick={() => setRemoving(c)}><Trash2 className="size-4" aria-hidden /></Button>
66
+ </li>
67
+ ))}
68
+ </ul>
69
+ )}
70
+ {remove.error ? <ApiError error={remove.error} title="Could not remove the sender" /> : null}
71
+ <ConfirmSpendDialog
72
+ open={removing != null}
73
+ tier="destructive"
74
+ title={`Remove ${removing?.channel_value ?? "this sender"} from the sequence?`}
75
+ description={others === 0 && removing ? `It is the only ${STEPS_LOST[typeOf(removing)] ?? ""} sender, so those steps can no longer send until you add another.` : "Other senders of the same kind keep the sequence sending."}
76
+ confirmLabel="Remove sender"
77
+ affected={1}
78
+ confirming={remove.loading}
79
+ onConfirm={() => removing && void remove.mutate(removing)}
80
+ onCancel={() => setRemoving(null)}
81
+ />
82
+ </div>
83
+ );
84
+ }