@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,103 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CrmRecords: look inside a connected CRM without leaving graph8. Contacts, Companies, Deals and Leads as lists (25 at a time, "Load more"
5
+ * pages through all of them), the CRM's own activity log, and a record's fields (plus a contact's activities) when you open one.
6
+ * Read only: changing CRM records from here is deliberately not offered (use the sync to push graph8 data; edit in the CRM itself).
7
+ * Not every CRM has every kind (Pipedrive has no leads in the same sense); a kind the CRM refuses shows its error with Retry.
8
+ *
9
+ * SDK operations, in order (connection_id from the connection):
10
+ * 1. listCrmContacts / listCrmCompanies / listCrmDeals / listCrmLeads GET /integrations/crm/connections/{connection_id}/{kind}?limit&offset[&name] (read)
11
+ * 2. getCrmContact / getCrmCompany / getCrmDeal / getCrmLead GET /integrations/crm/connections/{connection_id}/{kind}/{id} (read)
12
+ * 3. listCrmContactActivities GET /integrations/crm/connections/{connection_id}/contacts/{contact_id}/activities?limit (read)
13
+ * 4. listCrmActivities GET /integrations/crm/activities?connection_id&limit&offset (read)
14
+ */
15
+ import { useEffect, useState } from "react";
16
+ import { Button, Skeleton } from "@graph8/sdk/ui";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { EmptyState } from "@/g8/components/empty-state";
19
+ import { SectionCard } from "@/g8/components/screen/section-card";
20
+ import { Toolbar } from "@/g8/components/screen/toolbar";
21
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
22
+ import { g8Get } from "@/g8/lib/g8-client";
23
+ import { CRM_ENTITIES, crmRecordSubtitle, crmRecordTitle, flattenRecord } from "../lib/crm-utils";
24
+ import type { CrmEntity, CrmRecord } from "../lib/crm-utils";
25
+ import { FactList } from "../lib/facts";
26
+ import { LoadMore, useLoadMoreList } from "../lib/load-more";
27
+ import { arrayOf, formatWhen } from "../lib/integrations-utils";
28
+
29
+ export interface CrmRecordsProps {
30
+ connectionId: string;
31
+ className?: string;
32
+ }
33
+
34
+ type Tab = CrmEntity | "activity";
35
+ const enc = encodeURIComponent;
36
+
37
+ function RecordFacts({ connectionId, entity, id }: { connectionId: string; entity: CrmEntity; id: string }) {
38
+ const rec = useG8Query((signal) => g8Get<CrmRecord>(`/integrations/crm/connections/${enc(connectionId)}/${entity}/${enc(id)}`, undefined, { signal }), [connectionId, entity, id]);
39
+ const acts = useG8Query((signal) => g8Get(`/integrations/crm/connections/${enc(connectionId)}/contacts/${enc(id)}/activities`, { limit: 20 }, { signal }), [connectionId, id], { enabled: entity === "contacts" });
40
+ const data = ((rec.data as { data?: CrmRecord } | undefined)?.data ?? {}) as CrmRecord;
41
+ const rows = arrayOf<Record<string, unknown>>(acts.data);
42
+ return (
43
+ <SectionCard title={crmRecordTitle(data) || "Record"} description={rec.loading ? "Loading..." : crmRecordSubtitle(data) || undefined}>
44
+ {rec.error ? <ApiError error={rec.error} title="Could not load the record" onRetry={() => void rec.refetch()} /> : null}
45
+ {rec.loading && !rec.data ? <Skeleton className="h-16 w-full" /> : null}
46
+ {rec.data ? <FactList data={flattenRecord(data)} empty="This record has no fields we can show." /> : null}
47
+ {entity === "contacts" ? (
48
+ <div className="mt-4">
49
+ <h4 className="mb-1 text-sm font-medium">Recent activity</h4>
50
+ {acts.error ? <ApiError compact error={acts.error} title="Could not load the activities" onRetry={() => void acts.refetch()} /> : null}
51
+ {acts.loading && !acts.data ? <Skeleton className="h-8 w-full" /> : null}
52
+ {acts.data && rows.length === 0 ? <p className="text-sm text-muted-foreground">No activity in the CRM.</p> : null}
53
+ {rows.length > 0 ? <><ul className="divide-y rounded-md border text-sm">{rows.map((a, i) => <li key={String(a.id ?? i)} className="px-3 py-1.5">{String(a.subject ?? a.type ?? a.title ?? "Activity")}<span className="ml-2 text-xs text-muted-foreground">{formatWhen(String(a.timestamp ?? a.created_at ?? a.date ?? "") || null)}</span></li>)}</ul>{rows.length >= 20 ? <p className="mt-1 text-xs text-muted-foreground">Showing the latest 20 activities.</p> : null}</> : null}
54
+ </div>
55
+ ) : null}
56
+ </SectionCard>
57
+ );
58
+ }
59
+
60
+ export function CrmRecords({ connectionId, className }: CrmRecordsProps) {
61
+ const [tab, setTab] = useState<Tab>("contacts");
62
+ const [search, setSearch] = useState("");
63
+ const [open, setOpen] = useState<string | null>(null);
64
+ useEffect(() => { setOpen(null); setSearch(""); }, [connectionId, tab]);
65
+ const q = search.trim();
66
+ const list = useLoadMoreList<CrmRecord>(async (offset, limit, signal) => {
67
+ const base = `/integrations/crm/connections/${enc(connectionId)}`;
68
+ const res = tab === "activity" ? await g8Get("/integrations/crm/activities", { connection_id: connectionId, limit, offset }, { signal })
69
+ : await g8Get(`${base}/${tab}`, { limit, offset, name: tab === "contacts" ? q || undefined : undefined }, { signal });
70
+ return arrayOf<CrmRecord>(res);
71
+ }, [connectionId, tab, q], { pageSize: 25 });
72
+
73
+ return (
74
+ <div className={`flex flex-col gap-3 ${className ?? ""}`}>
75
+ <div role="tablist" aria-label="CRM record kinds" className="flex flex-wrap gap-1 border-b">
76
+ {([...CRM_ENTITIES, { id: "activity", label: "Activity log" }] as const).map((t) => (
77
+ <button key={t.id} type="button" role="tab" aria-selected={tab === t.id} onClick={() => setTab(t.id as Tab)} className={`px-3 py-2 text-sm ${tab === t.id ? "border-b-2 border-foreground font-medium" : "text-muted-foreground"}`}>{t.label}</button>
78
+ ))}
79
+ </div>
80
+ {tab === "contacts" ? <Toolbar search={{ value: search, onChange: setSearch, placeholder: "Search contacts by name" }} /> : null}
81
+ {list.error ? <ApiError error={list.error} title="Could not load these records" onRetry={list.reload} /> : null}
82
+ {list.loading ? <Skeleton className="h-24 w-full" aria-label="Loading records" /> : null}
83
+ <ul className="divide-y rounded-md border" aria-label="CRM records">
84
+ {list.items.map((r, i) => {
85
+ const id = String(r.id ?? "");
86
+ return tab === "activity" ? (
87
+ <li key={id || i} className="px-3 py-2 text-sm">{String(r.subject ?? r.type ?? r.title ?? "Activity")}<span className="ml-2 text-xs text-muted-foreground">{formatWhen(String(r.timestamp ?? r.created_at ?? r.date ?? "") || null)}</span></li>
88
+ ) : (
89
+ <li key={id || i} className={open === id ? "bg-accent" : ""}>
90
+ <button type="button" className="block w-full px-3 py-2 text-left text-sm" onClick={() => setOpen(open === id ? null : id)} disabled={!id}>
91
+ <span className="block font-medium">{crmRecordTitle(r)}</span><span className="block text-xs text-muted-foreground">{crmRecordSubtitle(r)}</span>
92
+ </button>
93
+ </li>
94
+ );
95
+ })}
96
+ </ul>
97
+ {!list.loading && !list.error && list.items.length === 0 ? <EmptyState compact title={q ? "No contacts match" : "Nothing here"} description={tab === "activity" ? "The CRM has no activity for this connection yet." : "This CRM has no records of this kind."} /> : null}
98
+ <LoadMore list={list} noun="records" />
99
+ {open && tab !== "activity" ? <RecordFacts key={`${tab}:${open}`} connectionId={connectionId} entity={tab} id={open} /> : null}
100
+ {open ? <Button size="sm" variant="ghost" className="self-start" onClick={() => setOpen(null)}>Close record</Button> : null}
101
+ </div>
102
+ );
103
+ }
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ /**
4
+ * FieldMappings: which fields exist on each side of the sync. The CRM's fields for a kind of record (contact, company, deal) next to the
5
+ * graph8 fields they can map to, each searchable. Read only: the mapping itself is chosen in graph8 (the API has no call to save one),
6
+ * so this view is for checking that a field you expect on the CRM side exists before you sync.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. discoverCrmFields GET /integrations/crm/fields?connection_id&entity_type (read) the CRM's fields
10
+ * 2. listMappableGraph8Fields GET /integrations/crm/fields/mappable?object_type (read) graph8 fields a CRM field can map to
11
+ */
12
+ import { useState } from "react";
13
+ import { Input, Skeleton } from "@graph8/sdk/ui";
14
+ import { ApiError } from "@/g8/components/api-error";
15
+ import { SectionCard } from "@/g8/components/screen/section-card";
16
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
17
+ import { g8Get } from "@/g8/lib/g8-client";
18
+ import { useBrand } from "@/g8/hooks/use-brand";
19
+ import { arrayOf } from "../lib/integrations-utils";
20
+
21
+ export interface FieldMappingsProps {
22
+ connectionId: string;
23
+ className?: string;
24
+ }
25
+
26
+ interface Field { name?: string; key?: string; label?: string; type?: string; field_type?: string }
27
+ // The API accepts only the plural object names (CrmFieldEntityType / Graph8FieldObjectType).
28
+ const KINDS = [{ id: "contacts", label: "Contacts" }, { id: "companies", label: "Companies" }, { id: "deals", label: "Deals" }];
29
+ const nameOf = (f: Field): string => f.label ?? f.name ?? f.key ?? "";
30
+
31
+ function FieldColumn({ title, query, filter }: { title: string; query: { data: unknown; error: unknown; loading: boolean; refetch: () => Promise<unknown> }; filter: string }) {
32
+ const rows = arrayOf<Field>(query.data).filter((f) => nameOf(f).toLowerCase().includes(filter.toLowerCase()));
33
+ return (
34
+ <SectionCard title={title} description={query.data ? `${rows.length} fields` : undefined} flush>
35
+ {query.error ? <div className="p-3"><ApiError error={query.error} title="Could not load the fields" onRetry={() => void query.refetch()} /></div> : null}
36
+ {query.loading && !query.data ? <Skeleton className="m-3 h-16" /> : null}
37
+ <ul className="max-h-80 divide-y overflow-y-auto text-sm">{rows.map((f, i) => <li key={`${nameOf(f)}-${i}`} className="flex justify-between gap-2 px-3 py-1.5"><span>{nameOf(f)}</span><span className="text-xs text-muted-foreground">{f.type ?? f.field_type ?? ""}</span></li>)}</ul>
38
+ {query.data && !query.error && rows.length === 0 ? <p className="px-3 py-2 text-sm text-muted-foreground">No fields match.</p> : null}
39
+ </SectionCard>
40
+ );
41
+ }
42
+
43
+ export function FieldMappings({ connectionId, className }: FieldMappingsProps) {
44
+ const brand = useBrand();
45
+ const [kind, setKind] = useState("contacts");
46
+ const [filter, setFilter] = useState("");
47
+ const crm = useG8Query((signal) => g8Get("/integrations/crm/fields", { connection_id: connectionId, entity_type: kind }, { signal }), [connectionId, kind]);
48
+ const mine = useG8Query((signal) => g8Get("/integrations/crm/fields/mappable", { object_type: kind }, { signal }), [kind]);
49
+ return (
50
+ <div className={`flex flex-col gap-3 ${className ?? ""}`}>
51
+ <div className="flex flex-wrap items-center gap-2">
52
+ <div role="group" aria-label="Record kind" className="flex gap-1">{KINDS.map((k) => <button key={k.id} type="button" aria-pressed={kind === k.id} onClick={() => setKind(k.id)} className={`rounded-md border px-3 py-1 text-sm ${kind === k.id ? "bg-foreground text-background" : ""}`}>{k.label}</button>)}</div>
53
+ <Input aria-label="Filter fields" className="h-8 max-w-xs" placeholder="Filter fields" value={filter} onChange={(e) => setFilter(e.target.value)} />
54
+ </div>
55
+ <div className="grid gap-4 md:grid-cols-2">
56
+ <FieldColumn title="In the CRM" query={crm} filter={filter} />
57
+ <FieldColumn title={`In ${brand.name}`} query={mine} filter={filter} />
58
+ </div>
59
+ </div>
60
+ );
61
+ }
@@ -26,17 +26,20 @@ import {
26
26
  arrayOf,
27
27
  CRM_PROVIDERS,
28
28
  formatWhen,
29
- GRAPH8_APP_INTEGRATIONS_URL,
29
+ GRAPH8_APP_INTEGRATIONS_PATH,
30
30
  looksConnected,
31
31
  PROVIDER_LABELS,
32
32
  } from "../lib/integrations-utils";
33
33
  import type { ConnectionRow, CrmProvider, CrmSyncRow } from "../lib/integrations-utils";
34
+ import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
34
35
 
35
36
  export interface IntegrationListProps {
36
37
  /** Where "Connect" sends the person. Default: graph8 Settings > Integrations. */
37
38
  connectUrl?: string;
38
39
  /** Called when the person opens a CRM's sync panel (render SyncStatus for it). */
39
40
  onOpenSync?: (provider: CrmProvider) => void;
41
+ /** Called when a person opens any CRM, connected or not (show a CrmDetail). */
42
+ onSelectCrm?: (provider: CrmProvider) => void;
40
43
  className?: string;
41
44
  }
42
45
 
@@ -51,7 +54,7 @@ const CHAT_TOOLS = [
51
54
 
52
55
  function Pill({ on, label }: { on: boolean; label?: string }) {
53
56
  return (
54
- <span className={`rounded px-2 py-0.5 text-xs font-medium ${on ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-700"}`}>
57
+ <span className={`rounded px-2 py-0.5 text-xs font-medium ${on ? "bg-positive text-positive-foreground" : "bg-muted text-foreground"}`}>
55
58
  {label ?? (on ? "Connected" : "Not connected")}
56
59
  </span>
57
60
  );
@@ -67,7 +70,7 @@ function ChatTools() {
67
70
  }, []);
68
71
  if (state.loading && !state.data) return <Skeleton className="h-10 w-full" />;
69
72
  return (
70
- <ul className="divide-y rounded border border-gray-200" aria-label="Workspace tools">
73
+ <ul className="divide-y rounded border border-border" aria-label="Workspace tools">
71
74
  {(state.data ?? []).map((t) => (
72
75
  <li key={t.key} className="flex items-center justify-between px-3 py-2 text-sm">
73
76
  <span>{t.label}</span>
@@ -78,7 +81,9 @@ function ChatTools() {
78
81
  );
79
82
  }
80
83
 
81
- export function IntegrationList({ connectUrl = GRAPH8_APP_INTEGRATIONS_URL, onOpenSync, className }: IntegrationListProps) {
84
+ export function IntegrationList({ connectUrl: connectUrlProp, onOpenSync, onSelectCrm, className }: IntegrationListProps) {
85
+ const brand = useBrand();
86
+ const connectUrl = connectUrlProp ?? brandUrl(brand, GRAPH8_APP_INTEGRATIONS_PATH);
82
87
  const [disconnecting, setDisconnecting] = useState<{ provider: CrmProvider; connection: ConnectionRow } | null>(null);
83
88
  const syncs = useG8Query((signal) => g8Get("/crm-syncs", undefined, { signal }), []);
84
89
  const connections = useG8Query((signal) => g8Get("/integrations/connections", undefined, { signal }), []);
@@ -105,7 +110,7 @@ export function IntegrationList({ connectUrl = GRAPH8_APP_INTEGRATIONS_URL, onOp
105
110
 
106
111
  return (
107
112
  <div className={`space-y-4 ${className ?? ""}`}>
108
- <ul className="divide-y rounded border border-gray-200" aria-label="CRM integrations">
113
+ <ul className="divide-y rounded border border-border" aria-label="CRM integrations">
109
114
  {CRM_PROVIDERS.map((p) => {
110
115
  const row = byProvider(p);
111
116
  const connected = Boolean(row && CONNECTED_STATES.includes(row.status?.toLowerCase()));
@@ -113,8 +118,8 @@ export function IntegrationList({ connectUrl = GRAPH8_APP_INTEGRATIONS_URL, onOp
113
118
  return (
114
119
  <li key={p} className="flex flex-wrap items-center justify-between gap-2 px-3 py-3" data-provider={p}>
115
120
  <div>
116
- <div className="font-medium">{PROVIDER_LABELS[p]}</div>
117
- <div className="text-xs text-gray-500">
121
+ {onSelectCrm ? <button type="button" className="font-medium hover:underline" onClick={() => onSelectCrm(p)}>{PROVIDER_LABELS[p]}</button> : <div className="font-medium">{PROVIDER_LABELS[p]}</div>}
122
+ <div className="text-xs text-muted-foreground">
118
123
  {connected ? `Last sync: ${formatWhen(row?.last_sync_at)}` : "Connect to sync contacts and companies."}
119
124
  </div>
120
125
  </div>
@@ -137,7 +142,7 @@ export function IntegrationList({ connectUrl = GRAPH8_APP_INTEGRATIONS_URL, onOp
137
142
  ) : (
138
143
  <Button size="sm" asChild>
139
144
  <a href={connectUrl} target="_blank" rel="noreferrer">
140
- Connect in graph8
145
+ Connect in {brand.linkLabel}
141
146
  </a>
142
147
  </Button>
143
148
  )}
@@ -0,0 +1,152 @@
1
+ "use client";
2
+
3
+ /**
4
+ * IntegrationsScreen: the finished integrations settings page. Two tabs:
5
+ * Connections the five CRMs and the workspace tools with their status (IntegrationList). Every CRM, connected or not, opens its
6
+ * complete detail screen (CrmDetail: connection, push/pull switches, job progress, manual push, disconnect; `?id=hubspot`).
7
+ * "Connect in graph8" links out: the OAuth consent screen only runs in the graph8 app.
8
+ * Webhooks create webhooks (WebhookManager); each opens its complete detail screen (WebhookDetail: every field, edit, deliveries,
9
+ * pause, rotate secret, delete; `?id=`). Every detail also opens as a full page with `?view=full`.
10
+ *
11
+ * Tools Slack, Google Workspace and Roam with live status, a connect link-out for each that is not connected, a Google connection
12
+ * test and a Drive folder browser (WorkspaceTools).
13
+ * Ad accounts the ad platforms graph8 can sync audiences to, which are connected, their accounts, and a warning for any the platform
14
+ * has paused (AdAccounts).
15
+ * A connected CRM's detail also has Health, Sync now, CRM records, Fields and Historic import tabs (see CrmDetail).
16
+ *
17
+ * SDK operations: two reads for the stat strip (GET /crm-syncs, GET /webhooks). Composes IntegrationList (read +
18
+ * destructive, confirmed), SyncStatus and SyncTriggers (external, confirmed), WebhookManager (external + destructive, confirmed),
19
+ * WorkspaceTools and AdAccounts (reads).
20
+ * Prerequisites: a CRM connected in the app (where: "app"); a public https route with G8_WEBHOOK_SECRET for webhooks.
21
+ */
22
+ import type { ReactNode } from "react";
23
+ import { useState } from "react";
24
+ import { Plug } from "lucide-react";
25
+ import { g8Get } from "@/g8/lib/g8-client";
26
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
27
+ import { useUrlState } from "@/g8/hooks/use-url-state";
28
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
29
+ import type { StatItem } from "@/g8/components/screen/stat-strip";
30
+ import { SplitView } from "@/g8/components/screen/split-view";
31
+ import { SectionCard } from "@/g8/components/screen/section-card";
32
+ import { arrayOf, CRM_PROVIDERS, GRAPH8_APP_INTEGRATIONS_PATH, PROVIDER_LABELS } from "../lib/integrations-utils";
33
+ import type { CrmProvider, CrmSyncRow } from "../lib/integrations-utils";
34
+ import { AdAccounts } from "./ad-accounts";
35
+ import { IntegrationList } from "./integration-list";
36
+ import { WorkspaceTools } from "./workspace-tools";
37
+ import { CrmDetail } from "./crm-detail";
38
+ import { WebhookDetail } from "./webhook-detail";
39
+ import { WebhookManager } from "./webhook-manager";
40
+ import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
41
+
42
+ export interface IntegrationsScreenProps {
43
+ /** Where "Connect" sends people. Default: graph8 Settings > Integrations. */
44
+ connectUrl?: string;
45
+ /** Pre-fill the webhook endpoint URL (your /api/webhooks/graph8 route). */
46
+ defaultWebhookUrl?: string;
47
+ /** Contact records (in the CRM's field names) offered for a manual push in the sync panel. */
48
+ pushRecords?: ReadonlyArray<Record<string, unknown>>;
49
+ toolbarActions?: ReactNode;
50
+ /** Buttons added to the open record's header: a CRM connection or a webhook. */
51
+ renderDetailActions?: (record: IntegrationRecord) => ReactNode;
52
+ /** Tabs added after the built-in ones on the open record. */
53
+ extraDetailTabs?: { id: string; label: string; render: (record: IntegrationRecord) => ReactNode }[];
54
+ className?: string;
55
+ }
56
+
57
+ /** The record open in the detail panel, passed to the slot props. */
58
+ export type IntegrationRecord = { kind: "crm"; provider: CrmProvider } | { kind: "webhook"; id: string };
59
+
60
+ const CONNECTED = ["connected", "active", "ok", "healthy", "syncing", "synced", "enabled", "linked"];
61
+
62
+ function useStats(): StatItem[] | undefined {
63
+ const syncs = useG8Query((signal) => g8Get("/crm-syncs", undefined, { signal }), []);
64
+ const hooks = useG8Query((signal) => g8Get("/webhooks", undefined, { signal }), []);
65
+ if (syncs.error) return undefined;
66
+ const rows = arrayOf<CrmSyncRow>(syncs.data);
67
+ const connected = rows.filter((r) => CONNECTED.includes(r.status?.toLowerCase())).length;
68
+ const items: StatItem[] = [
69
+ { label: "CRMs connected", value: `${connected} of ${CRM_PROVIDERS.length}`, tone: connected ? "positive" : "neutral" },
70
+ ];
71
+ if (!hooks.error && hooks.data) items.push({ label: "Webhooks", value: arrayOf<unknown>(hooks.data).length });
72
+ return syncs.data ? items : undefined;
73
+ }
74
+
75
+ export function IntegrationsScreen(props: IntegrationsScreenProps) {
76
+ const brand = useBrand();
77
+ const connectUrl = props.connectUrl ?? brandUrl(brand, GRAPH8_APP_INTEGRATIONS_PATH);
78
+ const [tabValue, setTab] = useUrlState("tab", "connections");
79
+ const tab = ["webhooks", "tools", "ads"].includes(tabValue) ? tabValue : "connections";
80
+ const [id, setId] = useUrlState("id", "");
81
+ const [view, setView] = useUrlState("view", "");
82
+ const [, setMigration] = useUrlState("migration", "");
83
+ const expanded = view === "full" && Boolean(id);
84
+ const [tick, setTick] = useState(0);
85
+ const stats = useStats();
86
+ const close = () => {
87
+ setMigration("");
88
+ setId("");
89
+ setView("");
90
+ };
91
+ const panel = {
92
+ onClose: close,
93
+ onExpand: expanded ? undefined : () => setView("full"),
94
+ onCollapse: expanded ? () => setView("") : undefined,
95
+ onChanged: () => setTick((t) => t + 1),
96
+ };
97
+ const provider = (CRM_PROVIDERS as readonly string[]).includes(id) ? (id as CrmProvider) : null;
98
+ const slots = (record: IntegrationRecord) => ({
99
+ extraActions: props.renderDetailActions?.(record),
100
+ extraTabs: (props.extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(record) })),
101
+ });
102
+ const split = (list: ReactNode, detail: ReactNode, open: boolean) => (
103
+ <SplitView detailOpen={open} onCloseDetail={close} expanded={expanded} onExpandedChange={(next) => setView(next ? "full" : "")} detailWidth="lg" list={list} detail={open ? detail : null} />
104
+ );
105
+
106
+ return (
107
+ <ScreenLayout
108
+ title="Integrations"
109
+ description={`Connect your CRM, keep it in sync, and send ${brand.linkLabel} events to your own systems.`}
110
+ icon={<Plug className="size-5" />}
111
+ actions={props.toolbarActions}
112
+ tabs={[
113
+ { id: "connections", label: "Connections" },
114
+ { id: "webhooks", label: "Webhooks" },
115
+ { id: "tools", label: "Tools" },
116
+ { id: "ads", label: "Ad accounts" },
117
+ ]}
118
+ activeTab={tab}
119
+ onTabChange={(next) => {
120
+ setTab(next);
121
+ close();
122
+ }}
123
+ stats={stats}
124
+ className={props.className}
125
+ >
126
+ {tab === "connections" ? (
127
+ <div className="space-y-4">
128
+ <SectionCard title="Connecting an account" description={`Sign-in with your CRM happens in ${brand.linkLabel}. Connect there, then manage the sync here.`}>
129
+ <a className="text-sm underline" href={connectUrl} target="_blank" rel="noreferrer">
130
+ Connect in {brand.linkLabel}
131
+ </a>
132
+ </SectionCard>
133
+ {split(
134
+ <IntegrationList key={tick} connectUrl={connectUrl} onSelectCrm={(p) => { setMigration(""); setId(p); }} onOpenSync={(p) => { setMigration(""); setId(p); }} />,
135
+ provider ? <CrmDetail key={provider} provider={provider} connectUrl={connectUrl} records={props.pushRecords} {...panel} {...slots({ kind: "crm", provider })} /> : null,
136
+ provider != null,
137
+ )}
138
+ </div>
139
+ ) : tab === "tools" ? (
140
+ <WorkspaceTools connectUrl={connectUrl} />
141
+ ) : tab === "ads" ? (
142
+ <AdAccounts connectUrl={connectUrl} />
143
+ ) : (
144
+ split(
145
+ <WebhookManager key={tick} defaultUrl={props.defaultWebhookUrl} onSelectWebhook={(h) => setId(h.id)} />,
146
+ <WebhookDetail key={id} webhookId={id} {...panel} {...slots({ kind: "webhook", id })} />,
147
+ Boolean(id),
148
+ )
149
+ )}
150
+ </ScreenLayout>
151
+ );
152
+ }
@@ -0,0 +1,133 @@
1
+ "use client";
2
+
3
+ /**
4
+ * MigrationPanel: bring a CRM's history into graph8, and keep it current afterwards. Three steps for a connected CRM:
5
+ * 1. Scan look at the CRM and count what is there (nothing is imported yet).
6
+ * 2. Review the counts per kind of record, the warnings, then Import (or Cancel). Importing copies the CRM's records into graph8.
7
+ * 3. Later "Sync changes" brings in only what changed in the CRM since the last import.
8
+ * Progress is polled until the run reaches a resting state (waiting for you, finished, failed or cancelled). The last completed import,
9
+ * if any, is shown first. Scanning and importing change data and read a lot from the CRM, so each goes through ConfirmSpendDialog
10
+ * (external); cancelling a run is destructive and confirmed. Salesforce record owners that are not in the workspace need a decision or the API
11
+ * refuses the confirm, so each unmatched owner is sent as an "unassigned" decision (their records are imported without an owner; nothing is
12
+ * invited or reassigned from here). Choosing something else per owner is done in the app.
13
+ *
14
+ * SDK operations, in order:
15
+ * 1. getLastCompletedMigration GET /integrations/connections/{connection_id}/last-migration (read)
16
+ * 2. startMigrationDiscovery POST /integrations/migrations/discovery { connection_id, crm_type } (external, confirmed)
17
+ * 3. getMigrationStatus GET /integrations/migrations/{migration_id} (read, polled)
18
+ * 4. confirmMigration POST /integrations/migrations/{migration_id}/confirm {} (external, confirmed)
19
+ * 5. cancelMigration POST /integrations/migrations/{migration_id}/cancel (destructive, confirmed)
20
+ * 6. startDeltaSync POST /integrations/migrations/delta-sync { connection_id, recover_missing: false } (external, confirmed)
21
+ * The id of a started run is kept in the page's address (?migration=), so reloading does not lose a run that is still going.
22
+ */
23
+ import { useEffect, useRef, useState } from "react";
24
+ import { Button, Skeleton } from "@graph8/sdk/ui";
25
+ import { ApiError } from "@/g8/components/api-error";
26
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
+ import { SectionCard } from "@/g8/components/screen/section-card";
28
+ import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
29
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
30
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
31
+ import { useJobPoll } from "@/g8/hooks/use-job-poll";
32
+ import { useUrlState } from "@/g8/hooks/use-url-state";
33
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
34
+ import { FactList } from "../lib/facts";
35
+ import { crmType, PROVIDER_LABELS } from "../lib/integrations-utils";
36
+ import { useBrand } from "@/g8/hooks/use-brand";
37
+ import type { CrmProvider } from "../lib/integrations-utils";
38
+
39
+ export interface MigrationPanelProps {
40
+ connectionId: string;
41
+ provider: CrmProvider;
42
+ className?: string;
43
+ }
44
+
45
+ interface Migration { migration_id?: string; status: string; phase?: string | null; overall_percent?: number; eta_seconds?: number | null; entities?: Record<string, Record<string, unknown> | number>; warnings?: Array<{ message?: string; code?: string }>; errors?: Array<{ message?: string; code?: string }>; owners?: Array<{ sf_user_id?: string; match?: { status?: string } }> }
46
+ type Ask = "scan" | "import" | "cancel" | "delta";
47
+ const REST = ["awaiting_confirmation", "paused", "completed", "failed", "cancelled"];
48
+ const enc = encodeURIComponent;
49
+ const text = (w: { message?: string; code?: string }): string => w.message ?? w.code ?? "See the app for details.";
50
+
51
+ /**
52
+ * Salesforce owners who are not in graph8 and have no match need a decision or the confirm is refused (422). The default keeps the import
53
+ * unattended and reversible: their records are imported unassigned (the "unassigned" action). Nothing is invited or reassigned from here.
54
+ */
55
+ export function ownerDecisions(owners: Migration["owners"]): Array<{ sf_user_id: string; action: "unassigned" }> {
56
+ return (owners ?? []).filter((o) => o.sf_user_id && o.match?.status === "unmatched").map((o) => ({ sf_user_id: o.sf_user_id as string, action: "unassigned" as const }));
57
+ }
58
+
59
+ export function MigrationPanel({ connectionId, provider, className }: MigrationPanelProps) {
60
+ const brand = useBrand();
61
+ const label = PROVIDER_LABELS[provider];
62
+ const [id, setId] = useUrlState("migration", "");
63
+ const [ask, setAsk] = useState<Ask | null>(null);
64
+ // A run belongs to the connection it was started for: switching connection drops it (and its URL id) so it can never be confirmed under another CRM.
65
+ const first = useRef(true);
66
+ useEffect(() => { setAsk(null); if (first.current) { first.current = false; return; } setId(""); }, [connectionId]); // eslint-disable-line react-hooks/exhaustive-deps
67
+ const last = useG8Query((signal) => g8Get(`/integrations/connections/${enc(connectionId)}/last-migration`, undefined, { signal }), [connectionId]);
68
+ const poll = useJobPoll<Migration>(async (signal) => (await g8Get<Migration>(`/integrations/migrations/${enc(id)}`, undefined, { signal })).data, { isTerminal: (m) => REST.includes(String(m.status).toLowerCase()), enabled: Boolean(id), intervalMs: 3000, maxIntervalMs: 15000 });
69
+ useEffect(() => { if (id) poll.start(); }, [id]); // eslint-disable-line react-hooks/exhaustive-deps
70
+ const m = poll.status;
71
+ const status = String(m?.status ?? "").toLowerCase();
72
+
73
+ const started = (res: { data?: { migration_id?: string; id?: string } }) => { const next = res.data?.migration_id ?? res.data?.id; if (next) setId(next); setAsk(null); };
74
+ const scan = useG8Mutation(() => g8Post<{ migration_id?: string }>("/integrations/migrations/discovery", { connection_id: connectionId, crm_type: crmType(provider) }), { onSuccess: started, onError: () => setAsk(null) });
75
+ const delta = useG8Mutation(() => g8Post<{ migration_id?: string }>("/integrations/migrations/delta-sync", { connection_id: connectionId, recover_missing: false }), { onSuccess: started, onError: () => setAsk(null) });
76
+ const confirm = useG8Mutation(() => g8Post(`/integrations/migrations/${enc(id)}/confirm`, decisions.length ? { owner_decisions: decisions } : {}), { onSuccess: () => { setAsk(null); poll.start(); }, onError: () => setAsk(null) });
77
+ const cancel = useG8Mutation(() => g8Post(`/integrations/migrations/${enc(id)}/cancel`, {}), { onSuccess: () => { setAsk(null); poll.start(); }, onError: () => setAsk(null) });
78
+ const busy = ask === "scan" ? scan.loading : ask === "import" ? confirm.loading : ask === "cancel" ? cancel.loading : delta.loading;
79
+ const lastRun = ((last.data as { data?: { migration?: Record<string, unknown> | null } } | undefined)?.data?.migration) ?? null;
80
+ const entities = Object.entries(m?.entities ?? {});
81
+ const decisions = ownerDecisions(m?.owners);
82
+
83
+ const COPY: Record<Ask, { tier: "external" | "destructive"; title: string; description: string; confirm: string }> = {
84
+ scan: { tier: "external", title: `Scan ${label}`, description: `${brand.name} reads ${label} to count what it holds. Nothing is imported until you confirm the import.`, confirm: "Scan" },
85
+ import: { tier: "external", title: `Import from ${label}`, description: `Copies the ${label} records counted below into ${brand.name} and can take a long time.${decisions.length ? ` ${decisions.length} record owner${decisions.length === 1 ? " is" : "s are"} not in ${brand.name}; their records are imported unassigned.` : ""}`, confirm: "Import" },
86
+ cancel: { tier: "destructive", title: "Cancel this import", description: `The run stops where it is. Records already copied stay in ${brand.name}.`, confirm: "Cancel import" },
87
+ delta: { tier: "external", title: `Sync changes from ${label}`, description: `Brings in only the records that changed in ${label} since the last import.`, confirm: "Sync changes" },
88
+ };
89
+ const c = ask ? COPY[ask] : null;
90
+ const run = () => void (ask === "scan" ? scan : ask === "import" ? confirm : ask === "cancel" ? cancel : delta).mutate(undefined);
91
+
92
+ return (
93
+ <div className={`flex flex-col gap-4 ${className ?? ""}`}>
94
+ <SectionCard title="Last completed import" actions={<Button size="sm" variant="ghost" onClick={() => void last.refetch()}>Recheck</Button>}>
95
+ {last.error ? <ApiError error={last.error} title="Could not check the last import" onRetry={() => void last.refetch()} /> : null}
96
+ {last.loading && !last.data ? <Skeleton className="h-8 w-full" /> : null}
97
+ {last.data && !last.error ? (lastRun ? <FactList data={lastRun} /> : <p className="text-sm text-muted-foreground">No import has completed for this connection.</p>) : null}
98
+ </SectionCard>
99
+ <div className="flex flex-wrap gap-2">
100
+ <Button onClick={() => setAsk("scan")}>Scan {label}</Button>
101
+ <Button variant="outline" onClick={() => setAsk("delta")}>Sync changes</Button>
102
+ </div>
103
+ {scan.error ? <ApiError error={scan.error} title="Could not start the scan" /> : null}
104
+ {delta.error ? <ApiError error={delta.error} title="Could not start the sync" /> : null}
105
+ {id ? (
106
+ <SectionCard title="Current run" description={id} actions={<Button size="sm" variant="ghost" onClick={() => setId("")}>Hide</Button>}>
107
+ {poll.error && !m ? <ApiError error={poll.error} title="Could not read the run" onRetry={() => poll.start()} /> : null}
108
+ {!m && !poll.error ? <Skeleton className="h-12 w-full" aria-label="Loading run" /> : null}
109
+ {m ? (
110
+ <div className="flex flex-col gap-3">
111
+ <div className="flex flex-wrap items-center gap-2">
112
+ <StatusPill tone={toneForStatus(status)} dot>{status.replace(/_/g, " ")}</StatusPill>
113
+ {m.phase ? <span className="text-xs text-muted-foreground">{String(m.phase).replace(/_/g, " ")}</span> : null}
114
+ {typeof m.overall_percent === "number" ? <span className="text-xs text-muted-foreground">{Math.round(m.overall_percent)}%</span> : null}
115
+ {poll.polling ? <span className="text-xs text-muted-foreground">updating</span> : null}
116
+ </div>
117
+ {entities.length ? <ul className="divide-y rounded-md border text-sm" aria-label="Records found">{entities.map(([k, v]) => <li key={k} className="flex justify-between px-3 py-1.5"><span>{k.replace(/_/g, " ")}</span><span className="text-muted-foreground">{typeof v === "number" ? v.toLocaleString() : Object.entries(v).filter(([, x]) => typeof x === "number").map(([n, x]) => `${n.replace(/_/g, " ")} ${(x as number).toLocaleString()}`).join(", ")}</span></li>)}</ul> : null}
118
+ {(m.warnings ?? []).map((w, i) => <p key={i} role="status" className="rounded-md border border-warning-foreground/25 bg-warning p-2 text-sm text-warning-foreground">{text(w)}</p>)}
119
+ {(m.errors ?? []).map((e, i) => <p key={i} role="alert" className="text-sm text-error-foreground">{text(e)}</p>)}
120
+ <div className="flex gap-2">
121
+ {status === "awaiting_confirmation" ? <Button onClick={() => setAsk("import")}>Import</Button> : null}
122
+ {!["completed", "failed", "cancelled"].includes(status) ? <Button variant="ghost" onClick={() => setAsk("cancel")}>Cancel</Button> : null}
123
+ </div>
124
+ {confirm.error ? <ApiError error={confirm.error} title="Could not start the import" /> : null}
125
+ {cancel.error ? <ApiError error={cancel.error} title="Could not cancel the run" /> : null}
126
+ </div>
127
+ ) : null}
128
+ </SectionCard>
129
+ ) : null}
130
+ <ConfirmSpendDialog open={c != null} tier={c?.tier ?? "external"} title={c?.title ?? ""} description={c?.description} confirmLabel={c?.confirm} confirming={busy} onConfirm={run} onCancel={() => setAsk(null)} />
131
+ </div>
132
+ );
133
+ }