@graph8/sdk 0.362.0 → 0.373.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 (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -0,0 +1,154 @@
1
+ "use client";
2
+
3
+ /**
4
+ * RecordConversationsTab: the email conversations with one contact, newest first, "Load more" for older ones. Opening a row
5
+ * shows its messages. Use it as an `extraDetailTabs` entry of ContactsScreen:
6
+ * { id: "conversations", label: "Conversations", render: (c) => <RecordConversationsTab email={c.work_email} onOpenThread={(sel) => router.push(`/inbox?id=${sel}`)} /> }
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. listInboxMailboxes GET /inbox/mailboxes (read; the mailboxes this key can read: search requires them)
10
+ * 2. searchInboxEmails POST /inbox/emails/search?page=&page_size= (read; body `{ mailboxes, search_emails: [email] }`, 10 per page)
11
+ * 3. getReply GET /inbox/{id}?channel=email (read; only when a row is opened)
12
+ * The inbox has no filter by contact id, so the contact is matched by email address (sender or recipient): `email` plus any
13
+ * `alsoEmails`. A contact with no usable address has no conversations to show. SMS and LinkedIn threads are not matched here.
14
+ *
15
+ * Hidden threads: the API answers a thread the key may not see exactly like one that does not exist (404, the same text).
16
+ * A row that 404s on open says "not available to you" instead of an error with a retry button.
17
+ */
18
+
19
+ import { useState } from "react";
20
+ import { Button, Skeleton } from "@graph8/sdk/ui";
21
+ import { MessagesSquare } from "lucide-react";
22
+ import { ApiError } from "@/g8/components/api-error";
23
+ import { EmptyState } from "@/g8/components/empty-state";
24
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
25
+ import { usePaginated } from "@/g8/hooks/use-paginated";
26
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
27
+ import { useInboxThread } from "../hooks/use-inbox-thread";
28
+ import { formatWhen, stripHtml, type InboxMessage } from "../lib/inbox-types";
29
+
30
+ const PAGE_SIZE = 10;
31
+
32
+ /** One thread of POST /inbox/emails/search. Only the fields shown here are read; all are optional. */
33
+ interface FoundThread { id?: string | number | null; email_id?: string | number | null; subject?: string | null; updated_at?: string | null; created_at?: string | null; status?: string | null; messages?: InboxMessage[] | null }
34
+
35
+ const idOf = (t: FoundThread): string => String(t.id ?? t.email_id ?? "");
36
+ const statusOf = (e: unknown): number | undefined => (e as { status?: number } | null)?.status;
37
+
38
+ /** Mailbox addresses from GET /inbox/mailboxes (rows are mailbox records; the address is `email`). */
39
+ function mailboxAddresses(env: unknown): string[] {
40
+ const data = (env as { data?: unknown } | null)?.data;
41
+ const rows = Array.isArray(data) ? data : ((data as { items?: unknown } | null)?.items ?? []);
42
+ const out = new Set<string>();
43
+ for (const r of Array.isArray(rows) ? rows : []) {
44
+ const row = r as { email?: string | null; mailbox?: string | null };
45
+ const address = (row.email ?? row.mailbox ?? "").trim();
46
+ if (address) out.add(address);
47
+ }
48
+ return [...out];
49
+ }
50
+
51
+ function Opened({ id }: { id: string }) {
52
+ const t = useInboxThread(id, "email");
53
+ if (t.error) {
54
+ return statusOf(t.error) === 404
55
+ ? <p role="status" className="p-3 text-sm text-muted-foreground">This conversation is not available to you. It may have been removed, or it belongs to a mailbox you cannot read.</p>
56
+ : <div className="p-3"><ApiError error={t.error} title="Could not load this conversation" onRetry={() => void t.refetch()} /></div>;
57
+ }
58
+ if (!t.data) return <Skeleton className="m-3 h-16 w-auto" aria-label="Loading conversation" />;
59
+ const messages = t.data.messages.filter((m) => !m.is_draft);
60
+ if (messages.length === 0) return <p className="p-3 text-sm text-muted-foreground">No messages in this conversation yet.</p>;
61
+ return (
62
+ <ol className="space-y-2 p-3" aria-label="Messages">
63
+ {messages.map((m, i) => (
64
+ <li key={m.message_id ?? i} className="rounded-md border p-3 text-sm">
65
+ <p className="mb-1 flex flex-wrap gap-2 text-xs text-muted-foreground"><span className="font-medium text-foreground">{m.from_address ?? "Unknown sender"}</span>{m.date ? <span>{formatWhen(m.date)}</span> : null}</p>
66
+ <p className="whitespace-pre-wrap">{stripHtml(m.content) || "(no content)"}</p>
67
+ </li>
68
+ ))}
69
+ </ol>
70
+ );
71
+ }
72
+
73
+ export interface RecordConversationsTabProps {
74
+ /** The contact's email address. Without one the tab explains there is nothing to match. */
75
+ email: string | null | undefined;
76
+ /** Other addresses of the contact (personal or additional emails; a comma-separated string is fine). Threads with any of them are shown. */
77
+ alsoEmails?: ReadonlyArray<string | null | undefined>;
78
+ /** Route to the conversation in your inbox screen; receives the inbox selection ("email:<id>"). Without it the rows link to `${inboxPath}?id=email:<id>`. */
79
+ onOpenThread?: (selection: string) => void;
80
+ /** Path of your inbox screen for the plain links. Default "/inbox". */
81
+ inboxPath?: string;
82
+ }
83
+
84
+ export function RecordConversationsTab({ email, alsoEmails = [], onOpenThread, inboxPath = "/inbox" }: RecordConversationsTabProps) {
85
+ const addresses = [...new Set([email, ...alsoEmails].flatMap((e) => (e ?? "").split(",")).map((e) => e.trim()).filter((e) => e !== "" && !e.includes("*") && e.includes("@")))];
86
+ const usable = addresses.length > 0;
87
+ const boxes = useG8Query(async (signal) => mailboxAddresses(await g8Get("/inbox/mailboxes", undefined, { signal })), [usable], { enabled: usable });
88
+ const mailboxes = boxes.data ?? [];
89
+
90
+ if (!usable) return <EmptyState compact icon={<MessagesSquare className="size-8" />} title="No email address" description="Conversations are matched by email address, and this contact has none to match." />;
91
+ if (boxes.error) return <ApiError error={boxes.error} title="Could not load mailboxes" onRetry={() => void boxes.refetch()} />;
92
+ if (!boxes.data) return <Skeleton className="h-24 w-full" aria-label="Loading conversations" />;
93
+ if (mailboxes.length === 0) return <EmptyState compact icon={<MessagesSquare className="size-8" />} title="No mailbox connected" description="Connect a mailbox to see the conversations you have with this contact." />;
94
+ // Mounted only once the mailboxes are known, so its first render is already "loading" (no empty-state flash before the first search).
95
+ return <ConversationList addresses={addresses} mailboxes={mailboxes} onOpenThread={onOpenThread} inboxPath={inboxPath} />;
96
+ }
97
+
98
+ interface ConversationListProps { addresses: string[]; mailboxes: string[]; onOpenThread?: (selection: string) => void; inboxPath: string }
99
+
100
+ function ConversationList({ addresses, mailboxes, onOpenThread, inboxPath }: ConversationListProps) {
101
+ const [open, setOpen] = useState<string | null>(null);
102
+ const list = usePaginated<FoundThread>(async (cursor, signal) => {
103
+ const page = cursor ? Number(cursor) || 1 : 1;
104
+ const env = await g8Post<{ items?: FoundThread[] }>(`/inbox/emails/search?page=${page}&page_size=${PAGE_SIZE}`, { mailboxes, search_emails: addresses }, { signal });
105
+ const items = (env.data?.items ?? []).filter((t) => idOf(t) !== "");
106
+ const more = (env as { pagination?: { has_next?: boolean } }).pagination?.has_next ?? items.length >= PAGE_SIZE;
107
+ return { data: items, pagination: { next_cursor: more && items.length > 0 ? String(page + 1) : null } } as never;
108
+ }, [addresses.join(","), mailboxes.join("|")]);
109
+
110
+ if (list.error) return <ApiError error={list.error} title="Could not load conversations" onRetry={() => void list.reload()} />;
111
+ if (list.loading && list.items.length === 0) return <Skeleton className="h-24 w-full" aria-label="Loading conversations" />;
112
+ if (list.items.length === 0) return <EmptyState compact icon={<MessagesSquare className="size-8" />} title="No conversations yet" description="Emails sent to or received from this contact show up here." />;
113
+
114
+ return (
115
+ <div className="space-y-2">
116
+ <ul className="divide-y rounded-md border" aria-label="Conversations">
117
+ {list.items.map((t) => {
118
+ const id = idOf(t);
119
+ const last = (t.messages ?? []).filter((m) => !m.is_draft).slice(-1)[0];
120
+ const isOpen = open === id;
121
+ const selection = `email:${id}`;
122
+ return (
123
+ <li key={id}>
124
+ <div className="flex items-center justify-between gap-3 p-3 text-sm">
125
+ <button type="button" aria-expanded={isOpen} className="min-w-0 flex-1 text-left" onClick={() => setOpen(isOpen ? null : id)}>
126
+ <span className="block truncate font-medium">{t.subject || "(no subject)"}</span>
127
+ <span className="block truncate text-xs text-muted-foreground">{[formatWhen(t.updated_at ?? t.created_at), last ? stripHtml(last.content).slice(0, 80) : null].filter(Boolean).join(" · ")}</span>
128
+ </button>
129
+ <a
130
+ href={`${inboxPath}?id=${encodeURIComponent(selection)}`}
131
+ className="shrink-0 text-xs text-muted-foreground hover:underline"
132
+ onClick={(e) => {
133
+ if (!onOpenThread || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
134
+ e.preventDefault();
135
+ onOpenThread(selection);
136
+ }}
137
+ >
138
+ Open in inbox
139
+ </a>
140
+ </div>
141
+ {isOpen ? <Opened id={id} /> : null}
142
+ </li>
143
+ );
144
+ })}
145
+ </ul>
146
+ {list.hasMore ? (
147
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
148
+ <span role="status">Showing {list.items.length.toLocaleString()} conversations</span>
149
+ <Button size="sm" variant="outline" loading={list.loadingMore} onClick={() => void list.loadMore()}>Load more</Button>
150
+ </div>
151
+ ) : null}
152
+ </div>
153
+ );
154
+ }
@@ -9,7 +9,13 @@
9
9
  * Returns { content (HTML), plain_content }; it delivers NOTHING.
10
10
  * 2. POST /inbox/{reply_id}/send (inbox.sendReply) EXTERNAL -> ConfirmSpendDialog first
11
11
  * Reaches a real person and cannot be recalled. A fixed Idempotency-Key per compose makes a double
12
- * click or a retry safe.
12
+ * click or a retry safe. Sends `from_address` = the thread's own sender (email mailbox, SMS number,
13
+ * LinkedIn account id) and `to` = the thread's contact, so the reply leaves from the mailbox the
14
+ * conversation is on. Before this the composer sent neither sender nor (for SMS) number, and the API
15
+ * refused every email reply with 400 "'from_address' (mailbox) is required".
16
+ * Only when an SMS or LinkedIn thread names no sender, one read fills a sender picker:
17
+ * GET /sms/phone-numbers (channels.listSmsPhoneNumbers) or
18
+ * GET /inbox/linkedin/accounts (aiInbox.listInboxLinkedinAccounts) READ
13
19
  *
14
20
  * Both calls are gated by tier: nothing is billed or sent until the person clicks the dialog's button.
15
21
  */
@@ -18,9 +24,10 @@ import { useIdempotencyKey } from "../lib/idempotency";
18
24
  import { Button, Input, Textarea } from "@graph8/sdk/ui";
19
25
  import { ApiError } from "@/g8/components/api-error";
20
26
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
- import { g8Post } from "@/g8/lib/g8-client";
27
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
22
28
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
- import { replyRecipient, stripHtml, type InboxThread } from "../lib/inbox-types";
29
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
30
+ import { replyRecipient, replySender, senderOptions, stripHtml, type InboxThread } from "../lib/inbox-types";
24
31
 
25
32
  type DraftOperation = "generate_from_scratch" | "shorten" | "expand" | "fix_grammar_spelling";
26
33
  type Pending = { kind: "draft"; operation: DraftOperation } | { kind: "send" } | null;
@@ -51,10 +58,22 @@ export function ReplyComposer({ thread, onSent, className }: ReplyComposerProps)
51
58
  const [body, setBody] = useState("");
52
59
  const [subject, setSubject] = useState(thread.subject ? (/^re:/i.test(thread.subject) ? thread.subject : `Re: ${thread.subject}`) : "");
53
60
  const [to, setTo] = useState(replyRecipient(thread));
61
+ const threadSender = replySender(thread);
62
+ const [from, setFrom] = useState(threadSender);
54
63
  const [tone, setTone] = useState<(typeof TONES)[number]>("professional");
55
64
  const [objective, setObjective] = useState<(typeof OBJECTIVES)[number]>("follow_up");
56
65
  const [pending, setPending] = useState<Pending>(null);
57
- const idempotency = useIdempotencyKey(JSON.stringify([thread.channel, thread.id, body, to, channel === "email" ? subject : null]));
66
+ const idempotency = useIdempotencyKey(JSON.stringify([thread.channel, thread.id, body, to, from, channel === "email" ? subject : null]));
67
+ // SMS and LinkedIn cannot send without a sender; when the thread does not name one, offer the org's.
68
+ const needsSenderPick = !threadSender && (channel === "sms" || channel === "linkedin");
69
+ const senders = useG8Query(
70
+ async (signal) => {
71
+ const path = channel === "sms" ? "/sms/phone-numbers" : "/inbox/linkedin/accounts";
72
+ return senderOptions(channel, (await g8Get<unknown>(path, undefined, { signal })).data);
73
+ },
74
+ [channel, needsSenderPick],
75
+ { enabled: needsSenderPick },
76
+ );
58
77
  const threadKey = `${thread.channel}:${thread.id}`;
59
78
  const currentThread = useRef(threadKey);
60
79
  currentThread.current = threadKey;
@@ -64,6 +83,7 @@ export function ReplyComposer({ thread, onSent, className }: ReplyComposerProps)
64
83
  setBody("");
65
84
  setSubject(thread.subject ? (/^re:/i.test(thread.subject) ? thread.subject : `Re: ${thread.subject}`) : "");
66
85
  setTo(replyRecipient(thread));
86
+ setFrom(replySender(thread));
67
87
  setPending(null);
68
88
  draft.reset();
69
89
  send.reset();
@@ -85,11 +105,18 @@ export function ReplyComposer({ thread, onSent, className }: ReplyComposerProps)
85
105
  const send = useG8Mutation(async () => {
86
106
  const env = await g8Post<{ message_id?: string | null; status: string }>(
87
107
  `/inbox/${encodeURIComponent(thread.id)}/send`,
88
- { body, channel, subject: channel === "email" ? subject : undefined, to: to || undefined },
108
+ {
109
+ body,
110
+ channel,
111
+ subject: channel === "email" ? subject : undefined,
112
+ to: to || undefined,
113
+ // Empty only for an email thread the list did not label; the API then uses the thread's mailbox.
114
+ from_address: from || undefined,
115
+ },
89
116
  { idempotencyKey: idempotency.get() },
90
117
  );
91
118
  return env.data;
92
- });
119
+ }, { invalidates: ["inbox"] });
93
120
 
94
121
  const confirm = async () => {
95
122
  const current = pending;
@@ -112,7 +139,8 @@ export function ReplyComposer({ thread, onSent, className }: ReplyComposerProps)
112
139
  };
113
140
 
114
141
  const busy = draft.loading || send.loading;
115
- const canSend = body.trim().length > 0 && (channel === "linkedin" || to.trim().length > 0) && !busy;
142
+ const hasSender = channel === "email" || from.trim().length > 0;
143
+ const canSend = body.trim().length > 0 && (channel === "linkedin" || to.trim().length > 0) && hasSender && !busy;
116
144
  const canTransform = body.trim().length > 0 && !busy;
117
145
  const canDraft = Boolean(draftChannel(channel));
118
146
 
@@ -121,7 +149,33 @@ export function ReplyComposer({ thread, onSent, className }: ReplyComposerProps)
121
149
  {channel === "email" ? (
122
150
  <Input aria-label="Subject" placeholder="Subject" value={subject} onChange={(e) => setSubject(e.target.value)} />
123
151
  ) : null}
124
- <Input aria-label="To" placeholder={channel === "sms" ? "Phone number" : "Recipient"} value={to} onChange={(e) => setTo(e.target.value)} />
152
+ {needsSenderPick ? (
153
+ <div className="space-y-1 text-sm">
154
+ <select
155
+ aria-label="Send from"
156
+ className="h-9 w-full rounded-md border bg-background px-2"
157
+ value={from}
158
+ disabled={senders.loading}
159
+ onChange={(e) => setFrom(e.target.value)}
160
+ >
161
+ <option value="">{senders.loading ? "Loading senders..." : channel === "sms" ? "Choose the number to text from" : "Choose the LinkedIn account to send from"}</option>
162
+ {(senders.data ?? []).map((o) => (
163
+ <option key={o.value} value={o.value}>
164
+ {o.label}
165
+ </option>
166
+ ))}
167
+ </select>
168
+ {senders.error ? <ApiError error={senders.error} title="Could not load your senders" onRetry={() => void senders.refetch()} /> : null}
169
+ {!senders.loading && !senders.error && senders.data?.length === 0 ? (
170
+ <p className="text-muted-foreground">{channel === "sms" ? "No SMS numbers on this account yet." : "No LinkedIn accounts connected yet."}</p>
171
+ ) : null}
172
+ </div>
173
+ ) : from ? (
174
+ <p className="text-xs text-muted-foreground">From {channel === "linkedin" ? "your LinkedIn account" : from}</p>
175
+ ) : null}
176
+ {channel !== "linkedin" ? (
177
+ <Input aria-label="To" placeholder={channel === "sms" ? "Phone number" : "Recipient"} value={to} onChange={(e) => setTo(e.target.value)} />
178
+ ) : null}
125
179
  <Textarea aria-label="Reply" rows={6} placeholder="Write your reply" value={body} onChange={(e) => setBody(e.target.value)} />
126
180
 
127
181
  {canDraft ? (
@@ -172,7 +226,9 @@ export function ReplyComposer({ thread, onSent, className }: ReplyComposerProps)
172
226
  open={pending?.kind === "send"}
173
227
  tier="external"
174
228
  title={`Send this ${channel} reply?`}
175
- description={`It goes to ${to || "the contact"} from your connected ${channel === "email" ? "mailbox" : channel} account.`}
229
+ description={`It goes to ${to || "the contact"} from ${
230
+ channel === "email" ? (from || "the thread's mailbox") : channel === "sms" ? from : "your LinkedIn account"
231
+ }.`}
176
232
  recipients={1}
177
233
  confirming={send.loading}
178
234
  onConfirm={() => void confirm()}
@@ -23,6 +23,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
23
  import { useG8Query } from "@/g8/hooks/use-g8-query";
24
24
  import type { InboxTag, InboxThread } from "../lib/inbox-types";
25
25
  import { ThreadBadge } from "./thread-badge";
26
+ import { pluralWord } from "@/g8/lib/plural";
26
27
 
27
28
  type Pending = { kind: "tag"; ids: string[] } | { kind: "assign"; email: string } | null;
28
29
  interface TeamUser {
@@ -70,9 +71,10 @@ export function TagAndAssign({ thread, availableTags = [], onChanged, className
70
71
  const tag = useG8Mutation(async (ids: string[]) => {
71
72
  // Only the NEW ids: association can fire tag automations, so already-applied tags are not re-sent.
72
73
  return g8Post(`/inbox/${encodeURIComponent(thread.id)}/tag`, { tag_ids: ids.filter((id) => !applied.has(id)) }, { query: channelQuery });
73
- });
74
+ }, { invalidates: ["inbox"] });
74
75
  const assign = useG8Mutation(async (email: string) =>
75
76
  g8Post(`/inbox/${encodeURIComponent(thread.id)}/assign`, { assignee_email: email }, { query: channelQuery }),
77
+ { invalidates: ["inbox"] },
76
78
  );
77
79
 
78
80
  const addTag = async () => {
@@ -132,7 +134,7 @@ export function TagAndAssign({ thread, availableTags = [], onChanged, className
132
134
  </Button>
133
135
  </div>
134
136
  <Button size="sm" disabled={selected.length === 0 || tag.loading} onClick={() => setPending({ kind: "tag", ids: selected })}>
135
- Apply {selected.length > 0 ? `${selected.length} ` : ""}tag{selected.length === 1 ? "" : "s"}
137
+ Apply {selected.length > 0 ? `${selected.length} ` : ""}{pluralWord(selected.length, "tag")}
136
138
  </Button>
137
139
  {createTag.error ? <ApiError error={createTag.error} title="Could not create the tag" /> : null}
138
140
  {tag.error ? <ApiError error={tag.error} title="Could not apply the tags" /> : null}
@@ -8,8 +8,8 @@ import type { ReactNode } from "react";
8
8
 
9
9
  const TONES = {
10
10
  neutral: "bg-muted text-foreground",
11
- info: "bg-blue-100 text-blue-900 dark:bg-blue-950 dark:text-blue-100",
12
- warn: "bg-amber-100 text-amber-900 dark:bg-amber-950 dark:text-amber-100",
11
+ info: "bg-info text-info-foreground",
12
+ warn: "bg-warning text-warning-foreground",
13
13
  } as const;
14
14
 
15
15
  export function ThreadBadge({ children, tone = "neutral" }: { children: ReactNode; tone?: keyof typeof TONES }) {
@@ -50,7 +50,7 @@ function MessageBubble({ message, outbound }: { message: InboxMessage; outbound:
50
50
  function ThreadHeader({ thread }: { thread: InboxThread }) {
51
51
  return (
52
52
  <header className="space-y-1 border-b pb-3">
53
- <h2 className="text-base font-semibold">{thread.subject || contactLabel(thread)}</h2>
53
+ <h2 className="text-base font-medium">{thread.subject || contactLabel(thread)}</h2>
54
54
  <p className="text-sm text-muted-foreground">
55
55
  {contactLabel(thread)}
56
56
  {thread.contact?.company ? ` at ${thread.contact.company}` : ""} · <span className="capitalize">{thread.channel}</span>
@@ -19,6 +19,6 @@ export function useInboxThread(threadId: string | null | undefined, channel: str
19
19
  return env.data;
20
20
  },
21
21
  [threadId, channel],
22
- { enabled: Boolean(threadId) },
22
+ { enabled: Boolean(threadId), key: ["inbox", "detail", threadId ?? null, channel ?? null] },
23
23
  );
24
24
  }
@@ -35,6 +35,7 @@ export function useInboxThreads(filters: InboxFilters, page = 1, pageSize = 25)
35
35
  return { threads: env.data ?? [], hasNext: Boolean(env.pagination?.has_next), total: (env.pagination as { total?: number } | undefined)?.total };
36
36
  },
37
37
  [filters.channel, filters.status, filters.tag, filters.sequenceId, page, pageSize],
38
+ { key: ["inbox", "list", { channel: filters.channel, status: filters.status, tag: filters.tag, sequenceId: filters.sequenceId, page, pageSize }] },
38
39
  );
39
40
  return { ...query, threads: query.data?.threads ?? [], hasNext: query.data?.hasNext ?? false, total: query.data?.total };
40
41
  }
@@ -46,6 +46,14 @@ export interface InboxThread {
46
46
  assignees: InboxAssignee[];
47
47
  created_at?: string | null;
48
48
  updated_at?: string | null;
49
+ /** Email: the connected mailbox the thread belongs to; a reply is sent from it. */
50
+ mailbox?: string | null;
51
+ /** SMS: your number the thread runs on; a reply is sent from it. */
52
+ from_number?: string | null;
53
+ /** SMS: the contact's number; a reply goes to it. */
54
+ to_number?: string | null;
55
+ /** LinkedIn: the account the conversation belongs to; a reply is sent from it. */
56
+ linkedin_account_id?: number | null;
49
57
  }
50
58
 
51
59
  /** Statuses `GET /inbox?status=` accepts (case-insensitive). Anything else is a 422. */
@@ -95,6 +103,44 @@ export function formatWhen(iso: string | null | undefined): string {
95
103
  /** Default recipient for a reply: the contact's address on the thread's channel. */
96
104
  export function replyRecipient(thread: InboxThread): string {
97
105
  const c = thread.contact;
98
- if (thread.channel === "sms") return c?.phone ?? "";
106
+ if (thread.channel === "sms") return thread.to_number || c?.phone || "";
99
107
  return c?.email ?? "";
100
108
  }
109
+
110
+ /**
111
+ * Default sender for a reply, as `POST /inbox/{id}/send` expects it in `from_address`: the thread's mailbox
112
+ * (email), your number on the thread (SMS) or the numeric LinkedIn account id. Empty when the thread does not
113
+ * say; the API then defaults email itself, and SMS/LinkedIn need a sender picked.
114
+ */
115
+ export function replySender(thread: InboxThread): string {
116
+ if (thread.channel === "sms") return thread.from_number ?? "";
117
+ if (thread.channel === "linkedin") return thread.linkedin_account_id != null ? String(thread.linkedin_account_id) : "";
118
+ return thread.mailbox ?? "";
119
+ }
120
+
121
+ export interface SenderOption {
122
+ value: string;
123
+ label: string;
124
+ }
125
+
126
+ /**
127
+ * Senders for a picker from `GET /sms/phone-numbers` or `GET /inbox/linkedin/accounts`. Both answer with an
128
+ * untyped `{ items: [...] }` envelope (no response schema in the contract), so this reads defensively.
129
+ */
130
+ export function senderOptions(channel: string, payload: unknown): SenderOption[] {
131
+ const raw = Array.isArray(payload) ? payload : ((payload as { items?: unknown[] } | null)?.items ?? []);
132
+ const out: SenderOption[] = [];
133
+ for (const item of raw as Array<Record<string, unknown>>) {
134
+ if (!item || typeof item !== "object") continue;
135
+ if (channel === "sms") {
136
+ const number = item.phone_number ?? item.number ?? item.phone;
137
+ if (typeof number === "string" && number) out.push({ value: number, label: String(item.friendly_name ?? item.label ?? number) });
138
+ continue;
139
+ }
140
+ const id = item.id ?? item.account_id ?? item.linkedin_account_id;
141
+ if (typeof id !== "number" && !(typeof id === "string" && /^\d+$/.test(id))) continue;
142
+ const name = [item.first_name ?? item.firstName, item.last_name ?? item.lastName].filter(Boolean).join(" ");
143
+ out.push({ value: String(id), label: String(name || item.name || item.email || item.emailAddress || `Account ${id}`) });
144
+ }
145
+ return out;
146
+ }
@@ -32,11 +32,21 @@ const draft = await g8.api.aiInbox.generateInboxDraft({
32
32
  });
33
33
  const html = (draft.data as { content?: string }).content ?? "";
34
34
 
35
- // 4. EXTERNAL: send. Reuse the same idempotency key if you retry this exact call.
35
+ // 4. EXTERNAL: send from the thread's own mailbox (`mailbox`) to its contact. Reuse the same idempotency key if
36
+ // you retry this exact call. When `from_address` / `to` are omitted the API fills both from the thread, but
37
+ // graph8 APIs older than contract 1.374.0 refuse the send with 400, so name them.
38
+ const mailbox = full.data.mailbox ?? undefined;
39
+ const contactEmail = (full.data.contact as { email?: string | null } | null | undefined)?.email ?? undefined;
36
40
  const sent = await g8.api.inbox.sendReply(
37
41
  {
38
42
  path: { reply_id: thread.id },
39
- body: { channel: "email", body: html, subject: full.data.subject ? `Re: ${full.data.subject}` : null },
43
+ body: {
44
+ channel: "email",
45
+ body: html,
46
+ subject: full.data.subject ? `Re: ${full.data.subject}` : null,
47
+ from_address: mailbox,
48
+ to: contactEmail,
49
+ },
40
50
  },
41
51
  { idempotencyKey: randomUUID() },
42
52
  );
@@ -5,6 +5,19 @@ Apollo / Salesforce "integrations" settings screen.
5
5
 
6
6
  Depends on: `foundation`. Install: `npx @graph8/sdk add integrations`.
7
7
 
8
+ ## Screens
9
+
10
+ `IntegrationsScreen` (`components/integrations-screen.tsx`) is the finished page: copy it as your route. No props are required.
11
+
12
+ - **Connections tab**: the five CRMs and the workspace tools. "Manage sync" opens a side panel with sync switches, job progress and a manual
13
+ push (deep link `?id=hubspot`; a webhook is `?tab=webhooks&id=<webhook id>`). Connecting is a link out to the graph8 app, because OAuth consent only runs there.
14
+ - A connected CRM's panel also has: **Health** (live status, connection record, CRM account, last historic import), **Sync now** (push or pull contacts, deals and leads as background jobs you can follow, with a lead push preview; each sync asks first), **CRM records** (browse contacts, companies, deals, leads and the activity log inside the CRM, read only), **Fields** (the CRM's fields beside graph8's) and **Historic import** (scan, review the counts, import, then sync later changes).
15
+ - **SugarCRM** is the one CRM the API can sign in to (instance address, user, password), so its panel has a **Connection** tab instead of a link-out; the other four link out to graph8.
16
+ - **Webhooks tab**: create, pause, rotate and delete webhooks, read deliveries. The secret is shown once.
17
+ - **Tools tab**: Slack, Google Workspace and Roam with live status and a "Connect in graph8" link for any that is not connected; Google also has a connection test and a Drive folder browser.
18
+ - **Ad accounts tab**: the ad platforms graph8 can sync audiences to, their accounts, a warning for any account the platform has paused, and a connect link-out.
19
+ - Props (all optional): `connectUrl`, `defaultWebhookUrl`, `pushRecords`, `toolbarActions`, `renderDetailActions(record)`, `extraDetailTabs` (each `{ id, label, render(record) }`), where `record` is `{ kind: "crm", provider }` or `{ kind: "webhook", id }`.
20
+
8
21
  ## Components
9
22
 
10
23
  | Component | What it does | Tiers |
@@ -12,6 +25,14 @@ Depends on: `foundation`. Install: `npx @graph8/sdk add integrations`.
12
25
  | `IntegrationList` | HubSpot, Salesforce, Pipedrive, Zoho and SugarCRM status, plus Slack, Google Workspace and Roam. Connect links to the app; Disconnect is confirmed. | read, destructive |
13
26
  | `SyncStatus` | One CRM: health, enable/disable continuous push and pull, job progress, manual contact push. | read, external |
14
27
  | `WebhookManager` | Create, pause, rotate and delete webhooks; secret shown once; delivery log; server-side verification note. | read, external, destructive |
28
+ | `ConnectionHealth` | One CRM connection: live status, record, account metadata, last historic import. | read |
29
+ | `SyncTriggers` | Run a sync now (contacts, deals, leads, push or pull), with a limit, a lead preview and job progress. | read, external |
30
+ | `CrmRecords` | Browse the records and activity log inside a connected CRM, read only. | read |
31
+ | `FieldMappings` | The CRM's fields next to the graph8 fields they can map to. | read |
32
+ | `MigrationPanel` | Scan a CRM, review, import its history, sync later changes. | read, external, destructive |
33
+ | `SugarCrmConnect` | Connect, check, update and remove a SugarCRM login. | read, external, destructive |
34
+ | `WorkspaceTools` | Slack, Google Workspace and Roam status, connect link-outs, Google test and Drive browser. | read |
35
+ | `AdAccounts` | Ad platforms, accounts and paused-account warnings. | read |
15
36
 
16
37
  `external` and `destructive` actions open `ConfirmSpendDialog` first (they write to the customer's CRM,
17
38
  change where data is sent, or remove a subscription).
@@ -41,7 +62,8 @@ reach the browser, which is why `WebhookManager` only displays it once at creati
41
62
 
42
63
  - CRM connected via the app's OAuth screen (`where: "app"`). Verify: `GET /crm-syncs/{provider}/status` -> `connected: true`.
43
64
  - A public https webhook route with `G8_WEBHOOK_SECRET` set (`where: "app"`).
44
- - The API key's user has integrations/webhooks admin rights (`where: "api"`).
65
+ - The API key's user has integrations/webhooks admin rights (`where: "api"`). Migrations and the SugarCRM sign-in need an organisation admin.
66
+ - The proxy `allowedPrefixes` include `integrations`, `crm-syncs`, `webhooks` and `audience-syncs` (the ad platform reads).
45
67
 
46
68
  ## Defaults (do not ask the human)
47
69
 
@@ -71,5 +93,45 @@ reach the browser, which is why `WebhookManager` only displays it once at creati
71
93
  | `rotate_webhook_secret_webhooks__webhook_id__rotate_secret_post` | POST /webhooks/{webhook_id}/rotate-secret | external | WebhookManager |
72
94
  | `list_webhook_deliveries_webhooks__webhook_id__deliveries_get` | GET /webhooks/{webhook_id}/deliveries | read | WebhookManager |
73
95
  | `delete_webhook_webhooks__webhook_id__delete` | DELETE /webhooks/{webhook_id} | destructive | WebhookManager |
96
+ | `cancel_migration_integrations_migrations__migration_id__cancel_post` | POST /integrations/migrations/{migration_id}/cancel | destructive | MigrationPanel |
97
+ | `confirm_migration_integrations_migrations__migration_id__confirm_post` | POST /integrations/migrations/{migration_id}/confirm | external | MigrationPanel |
98
+ | `disconnect_sugarcrm_integrations_sugarcrm_delete` | DELETE /integrations/sugarcrm | destructive | SugarCrmConnect |
99
+ | `discover_crm_fields_integrations_crm_fields_get` | GET /integrations/crm/fields | read | FieldMappings |
100
+ | `get_crm_company_integrations_crm_connections__connection_id__companies__company_id__get` | GET /integrations/crm/connections/{connection_id}/companies/{company_id} | read | CrmRecords |
101
+ | `get_crm_connection_integrations_crm_connections__connection_id__get` | GET /integrations/crm/connections/{connection_id} | read | ConnectionHealth |
102
+ | `get_crm_connection_metadata_integrations_crm_connections__connection_id__metadata_get` | GET /integrations/crm/connections/{connection_id}/metadata | read | ConnectionHealth |
103
+ | `get_crm_connection_status_integrations_crm_connections__connection_id__status_get` | GET /integrations/crm/connections/{connection_id}/status | read | ConnectionHealth |
104
+ | `get_crm_contact_integrations_crm_connections__connection_id__contacts__contact_id__get` | GET /integrations/crm/connections/{connection_id}/contacts/{contact_id} | read | CrmRecords |
105
+ | `get_crm_deal_integrations_crm_connections__connection_id__deals__deal_id__get` | GET /integrations/crm/connections/{connection_id}/deals/{deal_id} | read | CrmRecords |
106
+ | `get_crm_lead_integrations_crm_connections__connection_id__leads__lead_id__get` | GET /integrations/crm/connections/{connection_id}/leads/{lead_id} | read | CrmRecords |
107
+ | `get_last_completed_migration_integrations_connections__connection_id__last_migration_get` | GET /integrations/connections/{connection_id}/last-migration | read | ConnectionHealth, MigrationPanel |
108
+ | `get_migration_status_integrations_migrations__migration_id__get` | GET /integrations/migrations/{migration_id} | read | MigrationPanel |
109
+ | `get_webhook_webhooks__webhook_id__get` | GET /webhooks/{webhook_id} | read | WebhookDetail |
110
+ | `list_ad_account_health_audience_syncs_account_health_get` | GET /audience-syncs/account-health | read | AdAccounts |
111
+ | `list_ad_accounts_audience_syncs_accounts_get` | GET /audience-syncs/accounts | read | AdAccounts |
112
+ | `list_ad_platforms_audience_syncs_platforms_get` | GET /audience-syncs/platforms | read | AdAccounts |
113
+ | `list_crm_activities_integrations_crm_activities_get` | GET /integrations/crm/activities | read | CrmRecords |
114
+ | `list_crm_companies_integrations_crm_connections__connection_id__companies_get` | GET /integrations/crm/connections/{connection_id}/companies | read | CrmRecords |
115
+ | `list_crm_contact_activities_integrations_crm_connections__connection_id__contacts__contact_id__activities_get` | GET /integrations/crm/connections/{connection_id}/contacts/{contact_id}/activities | read | CrmRecords |
116
+ | `list_crm_contacts_integrations_crm_connections__connection_id__contacts_get` | GET /integrations/crm/connections/{connection_id}/contacts | read | CrmRecords |
117
+ | `list_crm_deals_integrations_crm_connections__connection_id__deals_get` | GET /integrations/crm/connections/{connection_id}/deals | read | CrmRecords |
118
+ | `list_crm_leads_integrations_crm_connections__connection_id__leads_get` | GET /integrations/crm/connections/{connection_id}/leads | read | CrmRecords |
119
+ | `list_google_drive_folder_files_integrations_google_workspace_folders__folder_id__files_get` | GET /integrations/google-workspace/folders/{folder_id}/files | read | WorkspaceTools |
120
+ | `list_google_drive_folders_integrations_google_workspace_folders_get` | GET /integrations/google-workspace/folders | read | WorkspaceTools |
121
+ | `list_lead_statuses_integrations_crm_sync_leads_statuses_get` | GET /integrations/crm-sync/leads/statuses | read | SyncTriggers |
122
+ | `list_mappable_graph8_fields_integrations_crm_fields_mappable_get` | GET /integrations/crm/fields/mappable | read | FieldMappings |
123
+ | `preview_lead_push_integrations_crm_sync_leads_push_preview_get` | GET /integrations/crm-sync/leads/push-preview | read | SyncTriggers |
124
+ | `pull_all_contacts_integrations_crm_sync_contacts_pull_post` | POST /integrations/crm-sync/contacts/pull | external | SyncTriggers |
125
+ | `pull_all_deals_integrations_crm_sync_deals_pull_post` | POST /integrations/crm-sync/deals/pull | external | SyncTriggers |
126
+ | `pull_all_leads_integrations_crm_sync_leads_pull_post` | POST /integrations/crm-sync/leads/pull | external | SyncTriggers |
127
+ | `push_all_contacts_integrations_crm_sync_contacts_push_post` | POST /integrations/crm-sync/contacts/push | external | SyncTriggers |
128
+ | `push_all_deals_integrations_crm_sync_deals_push_post` | POST /integrations/crm-sync/deals/push | external | SyncTriggers |
129
+ | `push_all_leads_integrations_crm_sync_leads_push_post` | POST /integrations/crm-sync/leads/push | external | SyncTriggers |
130
+ | `start_delta_sync_integrations_migrations_delta_sync_post` | POST /integrations/migrations/delta-sync | external | MigrationPanel |
131
+ | `start_migration_discovery_integrations_migrations_discovery_post` | POST /integrations/migrations/discovery | external | MigrationPanel |
132
+ | `test_google_workspace_connection_integrations_google_workspace_test_post` | POST /integrations/google-workspace/test | read | WorkspaceTools |
133
+ | `validate_sugarcrm_integrations_sugarcrm_validate_post` | POST /integrations/sugarcrm/validate | read | SugarCrmConnect |
134
+
135
+ Not offered on purpose: CRM pass-through writes (create, update and delete a contact, company, deal, lead or meeting directly in the CRM, and the batch versions) because they bypass graph8's records and field mapping (they stay in recipes and `g8.api.integrations`); the embedded connection session token and `saveConnection` (the OAuth consent screen only runs in the graph8 app); syncing a graph8 list or audience (needs a list picker and starts from the Lists screen); Google Ads conversions and video assets (the Ads screen owns audience syncs).
74
136
 
75
137
  Recipes (`recipes/`): `verify-webhook-route.ts`, `register-webhook.ts`, `crm-sync-health.ts`, `push-contacts-to-crm.ts`.